diff options
Diffstat (limited to 'apps/dav/lib')
410 files changed, 15460 insertions, 13383 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index deb28797952..9807b585080 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -3,37 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobia De Koninck <tobia@ledfan.be> - * - * @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\AppInfo; -use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin; +use OCA\DAV\CalDAV\CachedSubscriptionProvider; use OCA\DAV\CalDAV\CalendarManager; use OCA\DAV\CalDAV\CalendarProvider; use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider; @@ -41,10 +18,8 @@ use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; use OCA\DAV\CalDAV\Reminder\Notifier; - use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\ContactsManager; -use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\CardDAV\SyncService; use OCA\DAV\Events\AddressBookCreatedEvent; use OCA\DAV\Events\AddressBookDeletedEvent; @@ -53,12 +28,6 @@ use OCA\DAV\Events\AddressBookUpdatedEvent; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectCreatedEvent; -use OCA\DAV\Events\CalendarObjectDeletedEvent; -use OCA\DAV\Events\CalendarObjectMovedEvent; -use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectRestoredEvent; -use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarPublishedEvent; use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; @@ -69,8 +38,8 @@ use OCA\DAV\Events\CardDeletedEvent; use OCA\DAV\Events\CardUpdatedEvent; use OCA\DAV\Events\SubscriptionCreatedEvent; use OCA\DAV\Events\SubscriptionDeletedEvent; -use OCA\DAV\HookManager; use OCA\DAV\Listener\ActivityUpdaterListener; +use OCA\DAV\Listener\AddMissingIndicesListener; use OCA\DAV\Listener\AddressbookListener; use OCA\DAV\Listener\BirthdayListener; use OCA\DAV\Listener\CalendarContactInteractionListener; @@ -80,37 +49,56 @@ use OCA\DAV\Listener\CalendarPublicationListener; use OCA\DAV\Listener\CalendarShareUpdateListener; use OCA\DAV\Listener\CardListener; use OCA\DAV\Listener\ClearPhotoCacheListener; +use OCA\DAV\Listener\DavAdminSettingsListener; use OCA\DAV\Listener\OutOfOfficeListener; use OCA\DAV\Listener\SubscriptionListener; use OCA\DAV\Listener\TrustedServerRemovedListener; +use OCA\DAV\Listener\UserEventsListener; use OCA\DAV\Listener\UserPreferenceListener; use OCA\DAV\Search\ContactsSearchProvider; use OCA\DAV\Search\EventsSearchProvider; use OCA\DAV\Search\TasksSearchProvider; +use OCA\DAV\Settings\Admin\SystemAddressBookSettings; use OCA\DAV\SetupChecks\NeedsSystemAddressBookSync; use OCA\DAV\SetupChecks\WebdavEndpoint; use OCA\DAV\UserMigration\CalendarMigrator; use OCA\DAV\UserMigration\ContactsMigrator; use OCP\Accounts\UserUpdatedEvent; +use OCP\App\IAppManager; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\IAppContainer; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use OCP\Calendar\Events\CalendarObjectMovedEvent; +use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent; +use OCP\Calendar\Events\CalendarObjectRestoredEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\Calendar\IManager as ICalendarManager; use OCP\Config\BeforePreferenceDeletedEvent; use OCP\Config\BeforePreferenceSetEvent; use OCP\Contacts\IManager as IContactsManager; -use OCP\EventDispatcher\IEventDispatcher; +use OCP\DB\Events\AddMissingIndicesEvent; use OCP\Federation\Events\TrustedServerRemovedEvent; -use OCP\Files\AppData\IAppDataFactory; -use OCP\IUser; +use OCP\IUserSession; +use OCP\Server; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; +use OCP\User\Events\BeforeUserDeletedEvent; +use OCP\User\Events\BeforeUserIdUnassignedEvent; use OCP\User\Events\OutOfOfficeChangedEvent; use OCP\User\Events\OutOfOfficeClearedEvent; use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserCreatedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserFirstTimeLoggedInEvent; +use OCP\User\Events\UserIdAssignedEvent; +use OCP\User\Events\UserIdUnassignedEvent; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\GenericEvent; use Throwable; use function is_null; @@ -123,12 +111,6 @@ class Application extends App implements IBootstrap { public function register(IRegistrationContext $context): void { $context->registerServiceAlias('CardDAVSyncService', SyncService::class); - $context->registerService(PhotoCache::class, function (ContainerInterface $c) { - return new PhotoCache( - $c->get(IAppDataFactory::class)->get('dav-photocache'), - $c->get(LoggerInterface::class) - ); - }); $context->registerService(AppCalendarPlugin::class, function (ContainerInterface $c) { return new AppCalendarPlugin( $c->get(ICalendarManager::class), @@ -151,6 +133,8 @@ class Application extends App implements IBootstrap { /** * Register event listeners */ + $context->registerEventListener(AddMissingIndicesEvent::class, AddMissingIndicesListener::class); + $context->registerEventListener(CalendarCreatedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarDeletedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarDeletedEvent::class, CalendarObjectReminderUpdaterListener::class); @@ -204,63 +188,46 @@ class Application extends App implements IBootstrap { $context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class); $context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class); + $context->registerEventListener(UserFirstTimeLoggedInEvent::class, UserEventsListener::class); + $context->registerEventListener(UserIdAssignedEvent::class, UserEventsListener::class); + $context->registerEventListener(BeforeUserIdUnassignedEvent::class, UserEventsListener::class); + $context->registerEventListener(UserIdUnassignedEvent::class, UserEventsListener::class); + $context->registerEventListener(BeforeUserDeletedEvent::class, UserEventsListener::class); + $context->registerEventListener(UserDeletedEvent::class, UserEventsListener::class); + $context->registerEventListener(UserCreatedEvent::class, UserEventsListener::class); + $context->registerEventListener(UserChangedEvent::class, UserEventsListener::class); + $context->registerEventListener(UserUpdatedEvent::class, UserEventsListener::class); + $context->registerNotifierService(Notifier::class); $context->registerCalendarProvider(CalendarProvider::class); + $context->registerCalendarProvider(CachedSubscriptionProvider::class); $context->registerUserMigrator(CalendarMigrator::class); $context->registerUserMigrator(ContactsMigrator::class); $context->registerSetupCheck(NeedsSystemAddressBookSync::class); $context->registerSetupCheck(WebdavEndpoint::class); - } - public function boot(IBootContext $context): void { - // Load all dav apps - \OC_App::loadApps(['dav']); + // register admin settings form and listener(s) + $context->registerDeclarativeSettings(SystemAddressBookSettings::class); + $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, DavAdminSettingsListener::class); + $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, DavAdminSettingsListener::class); - $context->injectFn([$this, 'registerHooks']); - $context->injectFn([$this, 'registerContactsManager']); - $context->injectFn([$this, 'registerCalendarManager']); - $context->injectFn([$this, 'registerCalendarReminders']); } - public function registerHooks(HookManager $hm, - IEventDispatcher $dispatcher, - IAppContainer $container) { - $hm->setup(); - - // first time login event setup - $dispatcher->addListener(IUser::class . '::firstLogin', function ($event) use ($hm) { - if ($event instanceof GenericEvent) { - $hm->firstLogin($event->getSubject()); - } - }); - - $dispatcher->addListener(UserUpdatedEvent::class, function (UserUpdatedEvent $event) use ($container) { - /** @var SyncService $syncService */ - $syncService = \OCP\Server::get(SyncService::class); - $syncService->updateUser($event->getUser()); - }); - - - $dispatcher->addListener(CalendarShareUpdatedEvent::class, function (CalendarShareUpdatedEvent $event) use ($container) { - /** @var Backend $backend */ - $backend = $container->query(Backend::class); - $backend->onCalendarUpdateShares( - $event->getCalendarData(), - $event->getOldShares(), - $event->getAdded(), - $event->getRemoved() - ); + public function boot(IBootContext $context): void { + // Load all dav apps + $context->getServerContainer()->get(IAppManager::class)->loadApps(['dav']); - // Here we should recalculate if reminders should be sent to new or old sharees - }); + $context->injectFn($this->registerContactsManager(...)); + $context->injectFn($this->registerCalendarManager(...)); + $context->injectFn($this->registerCalendarReminders(...)); } public function registerContactsManager(IContactsManager $cm, IAppContainer $container): void { $cm->register(function () use ($container, $cm): void { - $user = \OC::$server->getUserSession()->getUser(); + $user = Server::get(IUserSession::class)->getUser(); if (!is_null($user)) { $this->setupContactsProvider($cm, $container, $user->getUID()); } else { @@ -278,18 +245,17 @@ class Application extends App implements IBootstrap { $cm->setupContactsProvider($contactsManager, $userID, $urlGenerator); } - private function setupSystemContactsProvider(IContactsManager $contactsManager, - IAppContainer $container): void { + private function setupSystemContactsProvider(IContactsManager $contactsManager, IAppContainer $container): void { /** @var ContactsManager $cm */ $cm = $container->query(ContactsManager::class); $urlGenerator = $container->getServer()->getURLGenerator(); - $cm->setupSystemContactsProvider($contactsManager, $urlGenerator); + $cm->setupSystemContactsProvider($contactsManager, null, $urlGenerator); } public function registerCalendarManager(ICalendarManager $calendarManager, IAppContainer $container): void { - $calendarManager->register(function () use ($container, $calendarManager) { - $user = \OC::$server->getUserSession()->getUser(); + $calendarManager->register(function () use ($container, $calendarManager): void { + $user = Server::get(IUserSession::class)->getUser(); if ($user !== null) { $this->setupCalendarProvider($calendarManager, $container, $user->getUID()); } diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php index 828818455f7..428547e3f61 100644 --- a/apps/dav/lib/AppInfo/PluginManager.php +++ b/apps/dav/lib/AppInfo/PluginManager.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\AppInfo; @@ -47,16 +28,6 @@ use function is_array; class PluginManager { /** - * @var ServerContainer - */ - private $container; - - /** - * @var IAppManager - */ - private $appManager; - - /** * App plugins * * @var ServerPlugin[] @@ -93,9 +64,10 @@ class PluginManager { * @param ServerContainer $container server container for resolving plugin classes * @param IAppManager $appManager app manager to loading apps and their info */ - public function __construct(ServerContainer $container, IAppManager $appManager) { - $this->container = $container; - $this->appManager = $appManager; + public function __construct( + private ServerContainer $container, + private IAppManager $appManager, + ) { } /** @@ -147,7 +119,7 @@ class PluginManager { $this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class); - foreach ($this->appManager->getInstalledApps() as $app) { + foreach ($this->appManager->getEnabledApps() as $app) { // load plugins and collections from info.xml $info = $this->appManager->getAppInfo($app); if (!isset($info['types']) || !in_array('dav', $info['types'], true)) { diff --git a/apps/dav/lib/Avatars/AvatarHome.php b/apps/dav/lib/Avatars/AvatarHome.php index 097307f452f..c3b95db1f4f 100644 --- a/apps/dav/lib/Avatars/AvatarHome.php +++ b/apps/dav/lib/Avatars/AvatarHome.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Avatars; @@ -33,20 +16,16 @@ use Sabre\Uri; class AvatarHome implements ICollection { - /** @var array */ - private $principalInfo; - /** @var IAvatarManager */ - private $avatarManager; - /** * AvatarHome constructor. * * @param array $principalInfo * @param IAvatarManager $avatarManager */ - public function __construct($principalInfo, IAvatarManager $avatarManager) { - $this->principalInfo = $principalInfo; - $this->avatarManager = $avatarManager; + public function __construct( + private $principalInfo, + private IAvatarManager $avatarManager, + ) { } public function createFile($name, $data = null) { diff --git a/apps/dav/lib/Avatars/AvatarNode.php b/apps/dav/lib/Avatars/AvatarNode.php index ade523561f2..b3a605fbb02 100644 --- a/apps/dav/lib/Avatars/AvatarNode.php +++ b/apps/dav/lib/Avatars/AvatarNode.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Avatars; @@ -26,10 +11,6 @@ use OCP\IAvatar; use Sabre\DAV\File; class AvatarNode extends File { - private $ext; - private $size; - private $avatar; - /** * AvatarNode constructor. * @@ -37,10 +18,11 @@ class AvatarNode extends File { * @param string $ext * @param IAvatar $avatar */ - public function __construct($size, $ext, $avatar) { - $this->size = $size; - $this->ext = $ext; - $this->avatar = $avatar; + public function __construct( + private $size, + private $ext, + private $avatar, + ) { } /** @@ -87,10 +69,6 @@ class AvatarNode extends File { } public function getLastModified() { - $timestamp = $this->avatar->getFile($this->size)->getMTime(); - if (!empty($timestamp)) { - return (int)$timestamp; - } - return $timestamp; + return $this->avatar->getFile($this->size)->getMTime(); } } diff --git a/apps/dav/lib/Avatars/RootCollection.php b/apps/dav/lib/Avatars/RootCollection.php index c5e78624d44..033dcaf7a5c 100644 --- a/apps/dav/lib/Avatars/RootCollection.php +++ b/apps/dav/lib/Avatars/RootCollection.php @@ -1,29 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Avatars; +use OCP\IAvatarManager; +use OCP\Server; use Sabre\DAVACL\AbstractPrincipalCollection; class RootCollection extends AbstractPrincipalCollection { @@ -39,7 +24,7 @@ class RootCollection extends AbstractPrincipalCollection { * @return AvatarHome */ public function getChildForPrincipal(array $principalInfo) { - $avatarManager = \OC::$server->getAvatarManager(); + $avatarManager = Server::get(IAvatarManager::class); return new AvatarHome($principalInfo, $avatarManager); } diff --git a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php index d1cafbf57c2..1165367c33f 100644 --- a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -41,41 +22,28 @@ use Psr\Log\LoggerInterface; */ class BuildReminderIndexBackgroundJob extends QueuedJob { - /** @var IDBConnection */ - private $db; - - /** @var ReminderService */ - private $reminderService; - - private LoggerInterface $logger; - - /** @var IJobList */ - private $jobList; - /** @var ITimeFactory */ private $timeFactory; /** * BuildReminderIndexBackgroundJob constructor. */ - public function __construct(IDBConnection $db, - ReminderService $reminderService, - LoggerInterface $logger, - IJobList $jobList, - ITimeFactory $timeFactory) { + public function __construct( + private IDBConnection $db, + private ReminderService $reminderService, + private LoggerInterface $logger, + private IJobList $jobList, + ITimeFactory $timeFactory, + ) { parent::__construct($timeFactory); - $this->db = $db; - $this->reminderService = $reminderService; - $this->logger = $logger; - $this->jobList = $jobList; $this->timeFactory = $timeFactory; } public function run($argument) { - $offset = (int) $argument['offset']; - $stopAt = (int) $argument['stopAt']; + $offset = (int)$argument['offset']; + $stopAt = (int)$argument['stopAt']; - $this->logger->info('Building calendar reminder index (' . $offset .'/' . $stopAt . ')'); + $this->logger->info('Building calendar reminder index (' . $offset . '/' . $stopAt . ')'); $offset = $this->buildIndex($offset, $stopAt); @@ -107,7 +75,7 @@ class BuildReminderIndexBackgroundJob extends QueuedJob { $result = $query->executeQuery(); while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { - $offset = (int) $row['id']; + $offset = (int)$row['id']; if (is_resource($row['calendardata'])) { $row['calendardata'] = stream_get_contents($row['calendardata']); } diff --git a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php index 96ceb644489..c6edac4f228 100644 --- a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php +++ b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -30,13 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; class CalendarRetentionJob extends TimedJob { - /** @var RetentionService */ - private $service; - - public function __construct(ITimeFactory $time, - RetentionService $service) { + public function __construct( + ITimeFactory $time, + private RetentionService $service, + ) { parent::__construct($time); - $this->service = $service; // Run four times a day $this->setInterval(6 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php index 073fc53e07a..49b6b1607ef 100644 --- a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php +++ b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -31,12 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; class CleanupDirectLinksJob extends TimedJob { - /** @var DirectMapper */ - private $mapper; - - public function __construct(ITimeFactory $timeFactory, DirectMapper $mapper) { + public function __construct( + ITimeFactory $timeFactory, + private DirectMapper $mapper, + ) { parent::__construct($timeFactory); - $this->mapper = $mapper; // Run once a day at off-peak time $this->setInterval(24 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php index 6339e721c93..7b664d03181 100644 --- a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php +++ b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -32,12 +14,11 @@ use OCP\IDBConnection; class CleanupInvitationTokenJob extends TimedJob { - /** @var IDBConnection */ - private $db; - - public function __construct(IDBConnection $db, ITimeFactory $time) { + public function __construct( + private IDBConnection $db, + ITimeFactory $time, + ) { parent::__construct($time); - $this->db = $db; // Run once a day at off-peak time $this->setInterval(24 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php new file mode 100644 index 00000000000..8a5e34381a7 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\QueuedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +class CleanupOrphanedChildrenJob extends QueuedJob { + public const ARGUMENT_CHILD_TABLE = 'childTable'; + public const ARGUMENT_PARENT_TABLE = 'parentTable'; + public const ARGUMENT_PARENT_ID = 'parentId'; + public const ARGUMENT_LOG_MESSAGE = 'logMessage'; + + private const BATCH_SIZE = 1000; + + public function __construct( + ITimeFactory $time, + private readonly IDBConnection $connection, + private readonly LoggerInterface $logger, + private readonly IJobList $jobList, + ) { + parent::__construct($time); + } + + protected function run($argument): void { + $childTable = $argument[self::ARGUMENT_CHILD_TABLE]; + $parentTable = $argument[self::ARGUMENT_PARENT_TABLE]; + $parentId = $argument[self::ARGUMENT_PARENT_ID]; + $logMessage = $argument[self::ARGUMENT_LOG_MESSAGE]; + + $orphanCount = $this->cleanUpOrphans($childTable, $parentTable, $parentId); + $this->logger->debug(sprintf($logMessage, $orphanCount)); + + // Requeue if there might be more orphans + if ($orphanCount >= self::BATCH_SIZE) { + $this->jobList->add(self::class, $argument); + } + } + + private function cleanUpOrphans( + string $childTable, + string $parentTable, + string $parentId, + ): int { + // We can't merge both queries into a single one here as DELETEing from a table while + // SELECTing it in a sub query is not supported by Oracle DB. + // Ref https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/delete.html#idm46006185488144 + + $selectQb = $this->connection->getQueryBuilder(); + + $selectQb->select('c.id') + ->from($childTable, 'c') + ->leftJoin('c', $parentTable, 'p', $selectQb->expr()->eq('c.' . $parentId, 'p.id')) + ->where($selectQb->expr()->isNull('p.id')) + ->setMaxResults(self::BATCH_SIZE); + + if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) { + $calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR; + $selectQb->andWhere($selectQb->expr()->eq('c.calendartype', $selectQb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + } + + $result = $selectQb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + if (empty($rows)) { + return 0; + } + + $orphanItems = array_map(static fn ($row) => $row['id'], $rows); + $deleteQb = $this->connection->getQueryBuilder(); + $deleteQb->delete($childTable) + ->where($deleteQb->expr()->in('id', $deleteQb->createNamedParameter($orphanItems, IQueryBuilder::PARAM_INT_ARRAY))); + $deleteQb->executeStatement(); + + return count($orphanItems); + } +} diff --git a/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php new file mode 100644 index 00000000000..bc306d58fe1 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +class DeleteOutdatedSchedulingObjects extends TimedJob { + public function __construct( + private CalDavBackend $calDavBackend, + private LoggerInterface $logger, + ITimeFactory $timeFactory, + ) { + parent::__construct($timeFactory); + $this->setInterval(23 * 60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); + } + + /** + * @param array $argument + */ + protected function run($argument): void { + $time = $this->time->getTime() - (60 * 60); + $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000); + $this->logger->info('Removed outdated scheduling objects'); + } +} diff --git a/apps/dav/lib/BackgroundJob/EventReminderJob.php b/apps/dav/lib/BackgroundJob/EventReminderJob.php index f628a728404..0e21e06fc35 100644 --- a/apps/dav/lib/BackgroundJob/EventReminderJob.php +++ b/apps/dav/lib/BackgroundJob/EventReminderJob.php @@ -3,29 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; +use OC\User\NoUserException; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException; use OCA\DAV\CalDAV\Reminder\ReminderService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; @@ -33,18 +18,12 @@ use OCP\IConfig; class EventReminderJob extends TimedJob { - /** @var ReminderService */ - private $reminderService; - - /** @var IConfig */ - private $config; - - public function __construct(ITimeFactory $time, - ReminderService $reminderService, - IConfig $config) { + public function __construct( + ITimeFactory $time, + private ReminderService $reminderService, + private IConfig $config, + ) { parent::__construct($time); - $this->reminderService = $reminderService; - $this->config = $config; // Run every 5 minutes $this->setInterval(5 * 60); @@ -52,9 +31,9 @@ class EventReminderJob extends TimedJob { } /** - * @throws \OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException - * @throws \OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException - * @throws \OC\User\NoUserException + * @throws ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + * @throws NoUserException */ public function run($argument):void { if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') { diff --git a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php index 220050b3927..6d94f4810ed 100644 --- a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -32,26 +15,19 @@ use OCP\IConfig; class GenerateBirthdayCalendarBackgroundJob extends QueuedJob { - /** @var BirthdayService */ - private $birthdayService; - - /** @var IConfig */ - private $config; - - public function __construct(ITimeFactory $time, - BirthdayService $birthdayService, - IConfig $config) { + public function __construct( + ITimeFactory $time, + private BirthdayService $birthdayService, + private IConfig $config, + ) { parent::__construct($time); - - $this->birthdayService = $birthdayService; - $this->config = $config; } public function run($argument) { $userId = $argument['userId']; $purgeBeforeGenerating = $argument['purgeBeforeGenerating'] ?? false; - // make sure admin didn't change his mind + // make sure admin didn't change their mind $isGloballyEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes'); if ($isGloballyEnabled !== 'yes') { return; diff --git a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php index 9b219cf30da..cc4fd5dce9d 100644 --- a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php +++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -58,7 +41,7 @@ class OutOfOfficeEventDispatcherJob extends QueuedJob { try { $absence = $this->absenceMapper->findById($id); - } catch (DoesNotExistException | \OCP\DB\Exception $e) { + } catch (DoesNotExistException|\OCP\DB\Exception $e) { $this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [ 'exception' => $e, 'argument' => $argument, diff --git a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php index d5a877a1742..8746588acc7 100644 --- a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php +++ b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -35,24 +18,21 @@ use Psr\Log\LoggerInterface; class PruneOutdatedSyncTokensJob extends TimedJob { - private IConfig $config; - private LoggerInterface $logger; - private CardDavBackend $cardDavBackend; - private CalDavBackend $calDavBackend; - - public function __construct(ITimeFactory $timeFactory, CalDavBackend $calDavBackend, CardDavBackend $cardDavBackend, IConfig $config, LoggerInterface $logger) { + public function __construct( + ITimeFactory $timeFactory, + private CalDavBackend $calDavBackend, + private CardDavBackend $cardDavBackend, + private IConfig $config, + private LoggerInterface $logger, + ) { parent::__construct($timeFactory); - $this->calDavBackend = $calDavBackend; - $this->cardDavBackend = $cardDavBackend; - $this->config = $config; - $this->logger = $logger; $this->setInterval(60 * 60 * 24); // One day $this->setTimeSensitivity(self::TIME_INSENSITIVE); } public function run($argument) { - $limit = max(1, (int) $this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000')); - $retention = max(7, (int) $this->config->getAppValue(Application::APP_ID, 'syncTokensRetentionDays', '60')) * 24 * 3600; + $limit = max(1, (int)$this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000')); + $retention = max(7, (int)$this->config->getAppValue(Application::APP_ID, 'syncTokensRetentionDays', '60')) * 24 * 3600; $prunedCalendarSyncTokens = $this->calDavBackend->pruneOutdatedSyncTokens($limit, $retention); $prunedAddressBookSyncTokens = $this->cardDavBackend->pruneOutdatedSyncTokens($limit, $retention); diff --git a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php index a40aeee3d66..e96735ca50b 100644 --- a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php +++ b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php @@ -3,29 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -62,8 +41,8 @@ class RefreshWebcalJob extends Job { $this->fixSubscriptionRowTyping($subscription); - // if no refresh rate was configured, just refresh once a week - $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1W'); + // if no refresh rate was configured, just refresh once a day + $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1D'); $refreshRate = $subscription[RefreshWebcalService::REFRESH_RATE] ?? $defaultRefreshRate; $subscriptionId = $subscription['id']; @@ -125,7 +104,7 @@ class RefreshWebcalJob extends Job { foreach ($forceInt as $column) { if (isset($row[$column])) { - $row[$column] = (int) $row[$column]; + $row[$column] = (int)$row[$column]; } } } diff --git a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php index 8c0b5e3ea45..7ec5b7fba79 100644 --- a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php +++ b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -35,12 +16,6 @@ use OCP\IUserManager; class RegisterRegenerateBirthdayCalendars extends QueuedJob { - /** @var IUserManager */ - private $userManager; - - /** @var IJobList */ - private $jobList; - /** * RegisterRegenerateBirthdayCalendars constructor. * @@ -48,19 +23,19 @@ class RegisterRegenerateBirthdayCalendars extends QueuedJob { * @param IUserManager $userManager * @param IJobList $jobList */ - public function __construct(ITimeFactory $time, - IUserManager $userManager, - IJobList $jobList) { + public function __construct( + ITimeFactory $time, + private IUserManager $userManager, + private IJobList $jobList, + ) { parent::__construct($time); - $this->userManager = $userManager; - $this->jobList = $jobList; } /** * @inheritDoc */ public function run($argument) { - $this->userManager->callForSeenUsers(function (IUser $user) { + $this->userManager->callForSeenUsers(function (IUser $user): void { $this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [ 'userId' => $user->getUID(), 'purgeBeforeGenerating' => true diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php index b4571e2509d..b7688ea32d8 100644 --- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php @@ -3,452 +3,32 @@ declare(strict_types=1); /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; -use OCA\DAV\CalDAV\CalDavBackend; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; -use OCP\Calendar\BackendTemporarilyUnavailableException; -use OCP\Calendar\IMetadataProvider; -use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Resource\IManager as IResourceManager; -use OCP\Calendar\Resource\IResource; use OCP\Calendar\Room\IManager as IRoomManager; -use OCP\Calendar\Room\IRoom; -use OCP\IDBConnection; class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { - - /** @var IResourceManager */ - private $resourceManager; - - /** @var IRoomManager */ - private $roomManager; - - /** @var IDBConnection */ - private $dbConnection; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(ITimeFactory $time, - IResourceManager $resourceManager, - IRoomManager $roomManager, - IDBConnection $dbConnection, - CalDavBackend $calDavBackend) { + public function __construct( + ITimeFactory $time, + private IResourceManager $resourceManager, + private IRoomManager $roomManager, + ) { parent::__construct($time); - $this->resourceManager = $resourceManager; - $this->roomManager = $roomManager; - $this->dbConnection = $dbConnection; - $this->calDavBackend = $calDavBackend; // Run once an hour $this->setInterval(60 * 60); $this->setTimeSensitivity(self::TIME_SENSITIVE); } - /** - * @param $argument - */ public function run($argument): void { - $this->runForBackend( - $this->resourceManager, - 'calendar_resources', - 'calendar_resources_md', - 'resource_id', - 'principals/calendar-resources' - ); - $this->runForBackend( - $this->roomManager, - 'calendar_rooms', - 'calendar_rooms_md', - 'room_id', - 'principals/calendar-rooms' - ); - } - - /** - * Run background-job for one specific backendManager - * either ResourceManager or RoomManager - * - * @param IResourceManager|IRoomManager $backendManager - * @param string $dbTable - * @param string $dbTableMetadata - * @param string $foreignKey - * @param string $principalPrefix - */ - private function runForBackend($backendManager, - string $dbTable, - string $dbTableMetadata, - string $foreignKey, - string $principalPrefix): void { - $backends = $backendManager->getBackends(); - - foreach ($backends as $backend) { - $backendId = $backend->getBackendIdentifier(); - - try { - if ($backend instanceof IResourceBackend) { - $list = $backend->listAllResources(); - } else { - $list = $backend->listAllRooms(); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); - $newIds = array_diff($list, $cachedList); - $deletedIds = array_diff($cachedList, $list); - $editedIds = array_intersect($list, $cachedList); - - foreach ($newIds as $newId) { - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($newId); - } else { - $resource = $backend->getRoom($newId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $id = $this->addToCache($dbTable, $backendId, $resource); - $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); - // we don't create the calendar here, it is created lazily - // when an event is actually scheduled with this resource / room - } - - foreach ($deletedIds as $deletedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); - $this->deleteFromCache($dbTable, $id); - $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); - - $principalName = implode('-', [$backendId, $deletedId]); - $this->deleteCalendarDataForResource($principalPrefix, $principalName); - } - - foreach ($editedIds as $editedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); - - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($editedId); - } else { - $resource = $backend->getRoom($editedId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $this->updateCache($dbTable, $id, $resource); - - if ($resource instanceof IMetadataProvider) { - $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); - $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); - } - } - } - } - - /** - * add entry to cache that exists remotely but not yet in cache - * - * @param string $table - * @param string $backendId - * @param IResource|IRoom $remote - * - * @return int Insert id - */ - private function addToCache(string $table, - string $backendId, - $remote): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - 'backend_id' => $query->createNamedParameter($backendId), - 'resource_id' => $query->createNamedParameter($remote->getId()), - 'email' => $query->createNamedParameter($remote->getEMail()), - 'displayname' => $query->createNamedParameter($remote->getDisplayName()), - 'group_restrictions' => $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - )) - ]) - ->executeStatement(); - return $query->getLastInsertId(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $foreignId - * @param array $metadata - */ - private function addMetadataToCache(string $table, - string $foreignKey, - int $foreignId, - array $metadata): void { - foreach ($metadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - $foreignKey => $query->createNamedParameter($foreignId), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - } - - /** - * delete entry from cache that does not exist anymore remotely - * - * @param string $table - * @param int $id - */ - private function deleteFromCache(string $table, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - */ - private function deleteMetadataFromCache(string $table, - string $foreignKey, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * update an existing entry in cache - * - * @param string $table - * @param int $id - * @param IResource|IRoom $remote - */ - private function updateCache(string $table, - int $id, - $remote): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($table) - ->set('email', $query->createNamedParameter($remote->getEMail())) - ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) - ->set('group_restrictions', $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - ))) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $dbTable - * @param string $foreignKey - * @param int $id - * @param array $metadata - * @param array $cachedMetadata - */ - private function updateMetadataCache(string $dbTable, - string $foreignKey, - int $id, - array $metadata, - array $cachedMetadata): void { - $newMetadata = array_diff_key($metadata, $cachedMetadata); - $deletedMetadata = array_diff_key($cachedMetadata, $metadata); - - foreach ($newMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($dbTable) - ->values([ - $foreignKey => $query->createNamedParameter($id), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - - foreach ($deletedMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($dbTable) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) - ->executeStatement(); - } - - $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); - foreach ($existingKeys as $existingKey) { - if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($dbTable) - ->set('value', $query->createNamedParameter($metadata[$existingKey])) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) - ->executeStatement(); - } - } - } - - /** - * serialize array of group restrictions to store them in database - * - * @param array $groups - * - * @return string - */ - private function serializeGroupRestrictions(array $groups): string { - return \json_encode($groups, JSON_THROW_ON_ERROR); - } - - /** - * Gets all metadata of a backend - * - * @param IResource|IRoom $resource - * - * @return array - */ - private function getAllMetadataOfBackend($resource): array { - if (!($resource instanceof IMetadataProvider)) { - return []; - } - - $keys = $resource->getAllAvailableMetadataKeys(); - $metadata = []; - foreach ($keys as $key) { - $metadata[$key] = $resource->getMetadataForKey($key); - } - - return $metadata; - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - * - * @return array - */ - private function getAllMetadataOfCache(string $table, - string $foreignKey, - int $id): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select(['key', 'value']) - ->from($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $metadata = []; - foreach ($rows as $row) { - $metadata[$row['key']] = $row['value']; - } - - return $metadata; - } - - /** - * Gets all cached rooms / resources by backend - * - * @param $tableName - * @param $backendId - * - * @return array - */ - private function getAllCachedByBackend(string $tableName, - string $backendId): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('resource_id') - ->from($tableName) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - return array_map(function ($row): string { - return $row['resource_id']; - }, $rows); - } - - /** - * @param $principalPrefix - * @param $principalUri - */ - private function deleteCalendarDataForResource(string $principalPrefix, - string $principalUri): void { - $calendar = $this->calDavBackend->getCalendarByUri( - implode('/', [$principalPrefix, $principalUri]), - CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); - - if ($calendar !== null) { - $this->calDavBackend->deleteCalendar( - $calendar['id'], - true // Because this wasn't deleted by a user - ); - } - } - - /** - * @param $table - * @param $backendId - * @param $resourceId - * - * @return int - */ - private function getIdForBackendAndResource(string $table, - string $backendId, - string $resourceId): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('id') - ->from($table) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); - $result = $query->executeQuery(); - - $id = (int) $result->fetchOne(); - $result->closeCursor(); - return $id; + $this->resourceManager->update(); + $this->roomManager->update(); } } diff --git a/apps/dav/lib/BackgroundJob/UploadCleanup.php b/apps/dav/lib/BackgroundJob/UploadCleanup.php index c35aff4d15a..230cde61578 100644 --- a/apps/dav/lib/BackgroundJob/UploadCleanup.php +++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php @@ -3,33 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; use OC\User\NoUserException; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\TimedJob; use OCP\Files\File; @@ -39,19 +19,17 @@ use OCP\Files\NotFoundException; use Psr\Log\LoggerInterface; class UploadCleanup extends TimedJob { - private IRootFolder $rootFolder; - private IJobList $jobList; - private LoggerInterface $logger; - - public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList, LoggerInterface $logger) { + public function __construct( + ITimeFactory $time, + private IRootFolder $rootFolder, + private IJobList $jobList, + private LoggerInterface $logger, + ) { parent::__construct($time); - $this->rootFolder = $rootFolder; - $this->jobList = $jobList; - $this->logger = $logger; // Run once a day $this->setInterval(60 * 60 * 24); - $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } protected function run($argument) { @@ -64,7 +42,7 @@ class UploadCleanup extends TimedJob { /** @var Folder $uploads */ $uploads = $userRoot->get('uploads'); $uploadFolder = $uploads->get($folder); - } catch (NotFoundException | NoUserException $e) { + } catch (NotFoundException|NoUserException $e) { $this->jobList->remove(self::class, $argument); return; } @@ -73,7 +51,7 @@ class UploadCleanup extends TimedJob { $time = $this->time->getTime() - 60 * 60 * 24; if (!($uploadFolder instanceof Folder)) { - $this->logger->error("Found a file inside the uploads folder. Uid: " . $uid . ' folder: ' . $folder); + $this->logger->error('Found a file inside the uploads folder. Uid: ' . $uid . ' folder: ' . $folder); if ($uploadFolder->getMTime() < $time) { $uploadFolder->delete(); } diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php index 5f88fa122b7..027b3349802 100644 --- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -2,23 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -43,18 +28,21 @@ use Sabre\VObject\Reader; use Sabre\VObject\Recur\RRuleIterator; class UserStatusAutomation extends TimedJob { - public function __construct(private ITimeFactory $timeFactory, + public function __construct( + private ITimeFactory $timeFactory, private IDBConnection $connection, private IJobList $jobList, private LoggerInterface $logger, private IManager $manager, private IConfig $config, private IAvailabilityCoordinator $coordinator, - private IUserManager $userManager) { + private IUserManager $userManager, + ) { parent::__construct($timeFactory); - // Interval 0 might look weird, but the last_checked is always moved - // to the next time we need this and then it's 0 seconds ago. + // interval = 0 might look odd, but it's intentional. last_run is set to + // the user's next available time, so the job runs immediately when + // that time comes. $this->setInterval(0); } @@ -70,14 +58,14 @@ class UserStatusAutomation extends TimedJob { $userId = $argument['userId']; $user = $this->userManager->get($userId); - if($user === null) { + if ($user === null) { return; } $ooo = $this->coordinator->getCurrentOutOfOfficeData($user); $continue = $this->processOutOfOfficeData($user, $ooo); - if($continue === false) { + if ($continue === false) { return; } @@ -211,20 +199,18 @@ class UserStatusAutomation extends TimedJob { return; } - if(!$hasDndForOfficeHours) { + if (!$hasDndForOfficeHours) { // Office hours are not set to DND, so there is nothing to do. return; } - $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND'); - // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. - $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->logger->debug('User is currently NOT available, reverting call and meeting status if applicable and then setting DND'); $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); $this->logger->debug('User status automation ran'); } private function processOutOfOfficeData(IUser $user, ?IOutOfOfficeData $ooo): bool { - if(empty($ooo)) { + if (empty($ooo)) { // Reset the user status if the absence doesn't exist $this->logger->debug('User has no OOO period in effect, reverting DND status if applicable'); $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); @@ -232,12 +218,12 @@ class UserStatusAutomation extends TimedJob { return true; } - if(!$this->coordinator->isInEffect($ooo)) { + if (!$this->coordinator->isInEffect($ooo)) { // Reset the user status if the absence is (no longer) in effect $this->logger->debug('User has no OOO period in effect, reverting DND status if applicable'); $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); - if($ooo->getStartDate() > $this->time->getTime()) { + if ($ooo->getStartDate() > $this->time->getTime()) { // Set the next run to take place at the start of the ooo period if it is in the future // This might be overwritten if there is an availability setting, but we can't determine // if this is the case here @@ -247,10 +233,8 @@ class UserStatusAutomation extends TimedJob { } $this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status'); - // Revert both a possible 'CALL - away' and 'office hours - DND' status - $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_CALL, IUserStatus::DND); - $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); $this->manager->setUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage()); + // Run at the end of an ooo period to return to availability / regular user status // If it's overwritten by a custom status in the meantime, there's nothing we can do about it $this->setLastRunToNextToggleTime($user->getUID(), $ooo->getEndDate()); diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 9890a615f93..d4faf3764e1 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me> - * - * @author Louis Chemineau <louis@chmn.me> - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * - * @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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\BulkUpload; @@ -34,15 +18,10 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class BulkUploadPlugin extends ServerPlugin { - private Folder $userFolder; - private LoggerInterface $logger; - public function __construct( - Folder $userFolder, - LoggerInterface $logger + private Folder $userFolder, + private LoggerInterface $logger, ) { - $this->userFolder = $userFolder; - $this->logger = $logger; } /** @@ -61,7 +40,7 @@ class BulkUploadPlugin extends ServerPlugin { */ public function httpPost(RequestInterface $request, ResponseInterface $response): bool { // Limit bulk upload to the /dav/bulk endpoint - if ($request->getPath() !== "bulk") { + if ($request->getPath() !== 'bulk') { return true; } @@ -94,16 +73,16 @@ class BulkUploadPlugin extends ServerPlugin { $node = $this->userFolder->getFirstNodeById($node->getId()); $writtenFiles[$headers['x-file-path']] = [ - "error" => false, - "etag" => $node->getETag(), - "fileid" => DavUtil::getDavFileId($node->getId()), - "permissions" => DavUtil::getDavPermissions($node), + 'error' => false, + 'etag' => $node->getETag(), + 'fileid' => DavUtil::getDavFileId($node->getId()), + 'permissions' => DavUtil::getDavPermissions($node), ]; } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['path' => $headers['x-file-path']]); $writtenFiles[$headers['x-file-path']] = [ - "error" => true, - "message" => $e->getMessage(), + 'error' => true, + 'message' => $e->getMessage(), ]; } } diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php index 7c977b42ccb..50f8cff76ba 100644 --- a/apps/dav/lib/BulkUpload/MultipartRequestParser.php +++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php @@ -1,23 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me> - * - * @author Louis Chemineau <louis@chmn.me> - * - * @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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\BulkUpload; @@ -35,10 +20,10 @@ class MultipartRequestParser { private $stream; /** @var string */ - private $boundary = ""; + private $boundary = ''; /** @var string */ - private $lastBoundary = ""; + private $lastBoundary = ''; /** * @throws BadRequest @@ -55,14 +40,14 @@ class MultipartRequestParser { } if ($contentType === null) { - throw new BadRequest("Content-Type can not be null"); + throw new BadRequest('Content-Type can not be null'); } $this->stream = $stream; $boundary = $this->parseBoundaryFromHeaders($contentType); - $this->boundary = '--'.$boundary."\r\n"; - $this->lastBoundary = '--'.$boundary."--\r\n"; + $this->boundary = '--' . $boundary . "\r\n"; + $this->lastBoundary = '--' . $boundary . "--\r\n"; } /** @@ -73,10 +58,16 @@ class MultipartRequestParser { */ private function parseBoundaryFromHeaders(string $contentType): string { try { + if (!str_contains($contentType, ';')) { + throw new \InvalidArgumentException('No semicolon in header'); + } [$mimeType, $boundary] = explode(';', $contentType); + if (!str_contains($boundary, '=')) { + throw new \InvalidArgumentException('No equal in boundary header'); + } [$boundaryKey, $boundaryValue] = explode('=', $boundary); } catch (\Exception $e) { - throw new BadRequest("Error while parsing boundary in Content-Type header.", Http::STATUS_BAD_REQUEST, $e); + throw new BadRequest('Error while parsing boundary in Content-Type header.', Http::STATUS_BAD_REQUEST, $e); } $boundaryValue = trim($boundaryValue); @@ -112,7 +103,7 @@ class MultipartRequestParser { $seekBackResult = fseek($this->stream, -$expectedContentLength, SEEK_CUR); if ($seekBackResult === -1) { - throw new Exception("Unknown error while seeking content", Http::STATUS_INTERNAL_SERVER_ERROR); + throw new Exception('Unknown error while seeking content', Http::STATUS_INTERNAL_SERVER_ERROR); } return $expectedContent === $content; @@ -150,7 +141,10 @@ class MultipartRequestParser { $headers = $this->readPartHeaders(); - $content = $this->readPartContent($headers["content-length"], $headers["x-file-md5"]); + $length = (int)$headers['content-length']; + + $this->validateHash($length, $headers['x-file-md5'] ?? '', $headers['oc-checksum'] ?? ''); + $content = $this->readPartContent($length); return [$headers, $content]; } @@ -162,7 +156,7 @@ class MultipartRequestParser { */ private function readBoundary(): string { if (!$this->isAtBoundary()) { - throw new BadRequest("Boundary not found where it should be."); + throw new BadRequest('Boundary not found where it should be.'); } return fread($this->stream, strlen($this->boundary)); @@ -196,12 +190,13 @@ class MultipartRequestParser { } } - if (!isset($headers["content-length"])) { - throw new LengthRequired("The Content-Length header must not be null."); + if (!isset($headers['content-length'])) { + throw new LengthRequired('The Content-Length header must not be null.'); } - if (!isset($headers["x-file-md5"])) { - throw new BadRequest("The X-File-MD5 header must not be null."); + // TODO: Drop $md5 condition when the latest desktop client that uses it is no longer supported. + if (!isset($headers['x-file-md5']) && !isset($headers['oc-checksum'])) { + throw new BadRequest('The hash headers must not be null.'); } return $headers; @@ -213,13 +208,7 @@ class MultipartRequestParser { * @throws Exception * @throws BadRequest */ - private function readPartContent(int $length, string $md5): string { - $computedMd5 = $this->computeMd5Hash($length); - - if ($md5 !== $computedMd5) { - throw new BadRequest("Computed md5 hash is incorrect."); - } - + private function readPartContent(int $length): string { if ($length === 0) { $content = ''; } else { @@ -231,7 +220,7 @@ class MultipartRequestParser { } if ($length !== 0 && feof($this->stream)) { - throw new Exception("Unexpected EOF while reading stream."); + throw new Exception('Unexpected EOF while reading stream.'); } // Read '\r\n'. @@ -241,12 +230,25 @@ class MultipartRequestParser { } /** - * Compute the MD5 hash of the next x bytes. + * Compute the MD5 or checksum hash of the next x bytes. + * TODO: Drop $md5 argument when the latest desktop client that uses it is no longer supported. */ - private function computeMd5Hash(int $length): string { - $context = hash_init('md5'); + private function validateHash(int $length, string $fileMd5Header, string $checksumHeader): void { + if ($checksumHeader !== '') { + [$algorithm, $hash] = explode(':', $checksumHeader, 2); + } elseif ($fileMd5Header !== '') { + $algorithm = 'md5'; + $hash = $fileMd5Header; + } else { + throw new BadRequest('No hash provided.'); + } + + $context = hash_init($algorithm); hash_update_stream($context, $this->stream, $length); fseek($this->stream, -$length, SEEK_CUR); - return hash_final($context); + $computedHash = hash_final($context); + if ($hash !== $computedHash) { + throw new BadRequest("Computed $algorithm hash is incorrect ($computedHash)."); + } } } diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php index 661a3dcc9ae..f0c49e6e28c 100644 --- a/apps/dav/lib/CalDAV/Activity/Backend.php +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity; @@ -45,27 +26,13 @@ use Sabre\VObject\Reader; */ class Backend { - /** @var IActivityManager */ - protected $activityManager; - - /** @var IGroupManager */ - protected $groupManager; - - /** @var IUserSession */ - protected $userSession; - - /** @var IAppManager */ - protected $appManager; - - /** @var IUserManager */ - protected $userManager; - - public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager, IUserManager $userManager) { - $this->activityManager = $activityManager; - $this->groupManager = $groupManager; - $this->userSession = $userSession; - $this->appManager = $appManager; - $this->userManager = $userManager; + public function __construct( + protected IActivityManager $activityManager, + protected IGroupManager $groupManager, + protected IUserSession $userSession, + protected IAppManager $appManager, + protected IUserManager $userManager, + ) { } /** @@ -153,7 +120,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('calendar', (int) $calendarData['id']) + ->setObject('calendar', (int)$calendarData['id']) ->setType('calendar') ->setAuthor($currentUser); @@ -181,7 +148,7 @@ class Backend { [ 'actor' => $currentUser, 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -212,7 +179,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('calendar', (int) $calendarData['id']) + ->setObject('calendar', (int)$calendarData['id']) ->setType('calendar') ->setAuthor($currentUser); @@ -237,7 +204,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -266,7 +233,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -308,7 +275,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -335,7 +302,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -413,7 +380,7 @@ class Backend { [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $properties['id'], + 'id' => (int)$properties['id'], 'uri' => $properties['uri'], 'name' => $properties['{DAV:}displayname'], ], @@ -463,7 +430,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('calendar', (int) $calendarData['id']) + ->setObject('calendar', (int)$calendarData['id']) ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo') ->setAuthor($currentUser); @@ -480,7 +447,7 @@ class Backend { $params = [ 'actor' => $event->getAuthor(), 'calendar' => [ - 'id' => (int) $calendarData['id'], + 'id' => (int)$calendarData['id'], 'uri' => $calendarData['uri'], 'name' => $calendarData['{DAV:}displayname'], ], @@ -554,7 +521,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('calendar', (int) $targetCalendarData['id']) + ->setObject('calendar', (int)$targetCalendarData['id']) ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo') ->setAuthor($currentUser); @@ -571,12 +538,12 @@ class Backend { $params = [ 'actor' => $event->getAuthor(), 'sourceCalendar' => [ - 'id' => (int) $sourceCalendarData['id'], + 'id' => (int)$sourceCalendarData['id'], 'uri' => $sourceCalendarData['uri'], 'name' => $sourceCalendarData['{DAV:}displayname'], ], 'targetCalendar' => [ - 'id' => (int) $targetCalendarData['id'], + 'id' => (int)$targetCalendarData['id'], 'uri' => $targetCalendarData['uri'], 'name' => $targetCalendarData['{DAV:}displayname'], ], @@ -623,9 +590,9 @@ class Backend { } if ($componentType === 'VEVENT') { - return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event']; + return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'event']; } - return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS]; + return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'todo', 'status' => (string)$component->STATUS]; } /** diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php index 06258e3cf74..78579ee84b7 100644 --- a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Filter; @@ -29,15 +12,10 @@ use OCP\IURLGenerator; class Calendar implements IFilter { - /** @var IL10N */ - protected $l; - - /** @var IURLGenerator */ - protected $url; - - public function __construct(IL10N $l, IURLGenerator $url) { - $this->l = $l; - $this->url = $url; + public function __construct( + protected IL10N $l, + protected IURLGenerator $url, + ) { } /** @@ -58,8 +36,8 @@ class Calendar implements IFilter { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php index 996250075c3..b001f90c28d 100644 --- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Filter; @@ -28,15 +12,10 @@ use OCP\IURLGenerator; class Todo implements IFilter { - /** @var IL10N */ - protected $l; - - /** @var IURLGenerator */ - protected $url; - - public function __construct(IL10N $l, IURLGenerator $url) { - $this->l = $l; - $this->url = $url; + public function __construct( + protected IL10N $l, + protected IURLGenerator $url, + ) { } /** @@ -57,8 +36,8 @@ class Todo implements IFilter { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php index 841011574d0..558abe0ca1a 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Provider; @@ -33,27 +16,19 @@ use OCP\IURLGenerator; use OCP\IUserManager; abstract class Base implements IProvider { - /** @var IUserManager */ - protected $userManager; - - /** @var IGroupManager */ - protected $groupManager; - /** @var string[] */ protected $groupDisplayNames = []; - /** @var IURLGenerator */ - protected $url; - /** * @param IUserManager $userManager * @param IGroupManager $groupManager - * @param IURLGenerator $urlGenerator + * @param IURLGenerator $url */ - public function __construct(IUserManager $userManager, IGroupManager $groupManager, IURLGenerator $urlGenerator) { - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->url = $urlGenerator; + public function __construct( + protected IUserManager $userManager, + protected IGroupManager $groupManager, + protected IURLGenerator $url, + ) { } protected function setSubjects(IEvent $event, string $subject, array $parameters): void { @@ -66,18 +41,18 @@ abstract class Base implements IProvider { * @return array */ protected function generateCalendarParameter($data, IL10N $l) { - if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI && - $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) { + if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI + && $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) { return [ 'type' => 'calendar', - 'id' => $data['id'], + 'id' => (string)$data['id'], 'name' => $l->t('Personal'), ]; } return [ 'type' => 'calendar', - 'id' => $data['id'], + 'id' => (string)$data['id'], 'name' => $data['name'], ]; } @@ -90,7 +65,7 @@ abstract class Base implements IProvider { protected function generateLegacyCalendarParameter($id, $name) { return [ 'type' => 'calendar', - 'id' => $id, + 'id' => (string)$id, 'name' => $name, ]; } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php index e4cc5fadf7c..8c93ddae431 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php @@ -1,31 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -48,18 +29,9 @@ class Calendar extends Base { public const SUBJECT_UNSHARE_USER = 'calendar_user_unshare'; public const SUBJECT_UNSHARE_GROUP = 'calendar_group_unshare'; - /** @var IFactory */ - protected $languageFactory; - /** @var IL10N */ protected $l; - /** @var IManager */ - protected $activityManager; - - /** @var IEventMerger */ - protected $eventMerger; - /** * @param IFactory $languageFactory * @param IURLGenerator $url @@ -68,11 +40,15 @@ class Calendar extends Base { * @param IGroupManager $groupManager * @param IEventMerger $eventMerger */ - public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger) { + public function __construct( + protected IFactory $languageFactory, + IURLGenerator $url, + protected IManager $activityManager, + IUserManager $userManager, + IGroupManager $groupManager, + protected IEventMerger $eventMerger, + ) { parent::__construct($userManager, $groupManager, $url); - $this->languageFactory = $languageFactory; - $this->activityManager = $activityManager; - $this->eventMerger = $eventMerger; } /** @@ -80,12 +56,12 @@ class Calendar extends Base { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException * @since 11.0.0 */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $this->l = $this->languageFactory->get('dav', $language); @@ -143,7 +119,7 @@ class Calendar extends Base { } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') { $subject = $this->l->t('{actor} unshared calendar {calendar} from group {group}'); } else { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $parsedParameters = $this->getParameters($event); diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php index 4dcb18fdb90..87551d7840b 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php @@ -1,32 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Provider; -use OC_App; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -45,21 +25,9 @@ class Event extends Base { public const SUBJECT_OBJECT_RESTORE = 'object_restore'; public const SUBJECT_OBJECT_DELETE = 'object_delete'; - /** @var IFactory */ - protected $languageFactory; - /** @var IL10N */ protected $l; - /** @var IManager */ - protected $activityManager; - - /** @var IEventMerger */ - protected $eventMerger; - - /** @var IAppManager */ - protected $appManager; - /** * @param IFactory $languageFactory * @param IURLGenerator $url @@ -69,19 +37,23 @@ class Event extends Base { * @param IEventMerger $eventMerger * @param IAppManager $appManager */ - public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger, IAppManager $appManager) { + public function __construct( + protected IFactory $languageFactory, + IURLGenerator $url, + protected IManager $activityManager, + IUserManager $userManager, + IGroupManager $groupManager, + protected IEventMerger $eventMerger, + protected IAppManager $appManager, + ) { parent::__construct($userManager, $groupManager, $url); - $this->languageFactory = $languageFactory; - $this->activityManager = $activityManager; - $this->eventMerger = $eventMerger; - $this->appManager = $appManager; } /** * @param array $eventData * @return array */ - protected function generateObjectParameter(array $eventData) { + protected function generateObjectParameter(array $eventData, string $affectedUser): array { if (!isset($eventData['id']) || !isset($eventData['name'])) { throw new \InvalidArgumentException(); } @@ -95,17 +67,21 @@ class Event extends Base { if (isset($eventData['link']) && is_array($eventData['link']) && $this->appManager->isEnabledForUser('calendar')) { try { // The calendar app needs to be manually loaded for the routes to be loaded - OC_App::loadApp('calendar'); + $this->appManager->loadApp('calendar'); $linkData = $eventData['link']; - $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $linkData['calendar_uri'] . '/' . $linkData['object_uri']); - $link = [ - 'view' => 'dayGridMonth', - 'timeRange' => 'now', - 'mode' => 'sidebar', + $calendarUri = $this->urlencodeLowerHex($linkData['calendar_uri']); + if ($affectedUser === $linkData['owner']) { + $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $calendarUri . '/' . $linkData['object_uri']); + } else { + // Can't use the "real" owner and calendar names here because we create a custom + // calendar for incoming shares with the name "<calendar>_shared_by_<sharer>". + // Hack: Fix the link by generating it for the incoming shared calendar instead, + // as seen from the affected user. + $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $affectedUser . '/' . $calendarUri . '_shared_by_' . $linkData['owner'] . '/' . $linkData['object_uri']); + } + $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexdirect.edit', [ 'objectId' => $objectId, - 'recurrenceId' => 'next' - ]; - $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexview.timerange.edit', $link); + ]); } catch (\Exception $error) { // Do nothing } @@ -118,12 +94,12 @@ class Event extends Base { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException * @since 11.0.0 */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_event') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $this->l = $this->languageFactory->get('dav', $language); @@ -159,7 +135,7 @@ class Event extends Base { } elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') { $subject = $this->l->t('You restored event {event} of calendar {calendar}'); } else { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $parsedParameters = $this->getParameters($event); @@ -189,7 +165,7 @@ class Event extends Base { return [ 'actor' => $this->generateUserParameter($parameters['actor']), 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), - 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_ADD . '_event_self': case self::SUBJECT_OBJECT_DELETE . '_event_self': @@ -198,7 +174,7 @@ class Event extends Base { case self::SUBJECT_OBJECT_RESTORE . '_event_self': return [ 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), - 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()), ]; } } @@ -210,13 +186,13 @@ class Event extends Base { 'actor' => $this->generateUserParameter($parameters['actor']), 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), - 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_MOVE . '_event_self': return [ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), - 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()), ]; } } @@ -233,25 +209,37 @@ class Event extends Base { return [ 'actor' => $this->generateUserParameter($parameters[0]), 'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]), - 'event' => $this->generateObjectParameter($parameters[2]), + 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_ADD . '_event_self': case self::SUBJECT_OBJECT_DELETE . '_event_self': case self::SUBJECT_OBJECT_UPDATE . '_event_self': return [ 'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]), - 'event' => $this->generateObjectParameter($parameters[2]), + 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()), ]; } throw new \InvalidArgumentException(); } - private function generateClassifiedObjectParameter(array $eventData) { - $parameter = $this->generateObjectParameter($eventData); + private function generateClassifiedObjectParameter(array $eventData, string $affectedUser): array { + $parameter = $this->generateObjectParameter($eventData, $affectedUser); if (!empty($eventData['classified'])) { $parameter['name'] = $this->l->t('Busy'); } return $parameter; } + + /** + * Return urlencoded string but with lower cased hex sequences. + * The remaining casing will be untouched. + */ + private function urlencodeLowerHex(string $raw): string { + return preg_replace_callback( + '/%[0-9A-F]{2}/', + static fn (array $matches) => strtolower($matches[0]), + urlencode($raw), + ); + } } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php index 0a8434b9595..fc0625ec970 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php @@ -1,29 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; class Todo extends Event { @@ -33,12 +16,12 @@ class Todo extends Event { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException * @since 11.0.0 */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_todo') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $this->l = $this->languageFactory->get('dav', $language); @@ -74,7 +57,7 @@ class Todo extends Event { } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') { $subject = $this->l->t('You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}'); } else { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $parsedParameters = $this->getParameters($event); @@ -104,7 +87,7 @@ class Todo extends Event { return [ 'actor' => $this->generateUserParameter($parameters['actor']), 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), - 'todo' => $this->generateObjectParameter($parameters['object']), + 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_ADD . '_todo_self': case self::SUBJECT_OBJECT_DELETE . '_todo_self': @@ -113,7 +96,7 @@ class Todo extends Event { case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': return [ 'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l), - 'todo' => $this->generateObjectParameter($parameters['object']), + 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()), ]; } } @@ -125,13 +108,13 @@ class Todo extends Event { 'actor' => $this->generateUserParameter($parameters['actor']), 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), - 'todo' => $this->generateObjectParameter($parameters['object']), + 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_MOVE . '_todo_self': return [ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), - 'todo' => $this->generateObjectParameter($parameters['object']), + 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()), ]; } } @@ -150,7 +133,7 @@ class Todo extends Event { return [ 'actor' => $this->generateUserParameter($parameters[0]), 'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]), - 'todo' => $this->generateObjectParameter($parameters[2]), + 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()), ]; case self::SUBJECT_OBJECT_ADD . '_todo_self': case self::SUBJECT_OBJECT_DELETE . '_todo_self': @@ -159,7 +142,7 @@ class Todo extends Event { case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self': return [ 'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]), - 'todo' => $this->generateObjectParameter($parameters[2]), + 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()), ]; } diff --git a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php index 20325a253f4..7ab7f16dbbb 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Setting; @@ -30,14 +12,12 @@ use OCP\Activity\ActivitySettings; use OCP\IL10N; abstract class CalDAVSetting extends ActivitySettings { - /** @var IL10N */ - protected $l; - /** * @param IL10N $l */ - public function __construct(IL10N $l) { - $this->l = $l; + public function __construct( + protected IL10N $l, + ) { } public function getGroupIdentifier() { diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php index 4a226fca439..0ad86a919bc 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Setting; @@ -42,8 +25,8 @@ class Calendar extends CalDAVSetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Event.php b/apps/dav/lib/CalDAV/Activity/Setting/Event.php index 0239296a403..ea9476d6f08 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Event.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Setting; @@ -42,8 +25,8 @@ class Event extends CalDAVSetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php index a4ac3f5d569..ed8377b0ffa 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Activity\Setting; @@ -43,8 +26,8 @@ class Todo extends CalDAVSetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php index 68b71404371..87d26324c32 100644 --- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php +++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de> - * - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\AppCalendar; @@ -41,12 +24,14 @@ use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Reader; class AppCalendar extends ExternalCalendar { - protected string $principal; protected ICalendar $calendar; - public function __construct(string $appId, ICalendar $calendar, string $principal) { + public function __construct( + string $appId, + ICalendar $calendar, + protected string $principal, + ) { parent::__construct($appId, $calendar->getUri()); - $this->principal = $principal; $this->calendar = $calendar; } diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php index ddf76e27f3a..72f2ed2c163 100644 --- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php +++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php @@ -3,29 +3,14 @@ declare(strict_types=1); /** - * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de> - * - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\AppCalendar; +use OCA\DAV\CalDAV\CachedSubscriptionImpl; +use OCA\DAV\CalDAV\CalendarImpl; use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCP\Calendar\IManager; @@ -33,12 +18,10 @@ use Psr\Log\LoggerInterface; /* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */ class AppCalendarPlugin implements ICalendarProvider { - protected IManager $manager; - protected LoggerInterface $logger; - - public function __construct(IManager $manager, LoggerInterface $logger) { - $this->manager = $manager; - $this->logger = $logger; + public function __construct( + protected IManager $manager, + protected LoggerInterface $logger, + ) { } public function getAppID(): string { @@ -68,7 +51,7 @@ class AppCalendarPlugin implements ICalendarProvider { return array_values( array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) { // We must not provide a wrapper for DAV calendars - return ! ($c instanceof \OCA\DAV\CalDAV\CalendarImpl); + return ! (($c instanceof CalendarImpl) || ($c instanceof CachedSubscriptionImpl)); }) ); } diff --git a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php index bfcab35f74e..3c62a26df54 100644 --- a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php +++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de> - * - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\AppCalendar; @@ -37,14 +20,11 @@ use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Property\ICalendar\DateTime; class CalendarObject implements ICalendarObject, IACL { - private VCalendar $vobject; - private AppCalendar $calendar; - private ICalendar|ICreateFromString $backend; - - public function __construct(AppCalendar $calendar, ICalendar $backend, VCalendar $vobject) { - $this->backend = $backend; - $this->calendar = $calendar; - $this->vobject = $vobject; + public function __construct( + private AppCalendar $calendar, + private ICalendar|ICreateFromString $backend, + private VCalendar $vobject, + ) { } public function getOwner() { diff --git a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php index 89e50c7da6b..71b9acb939b 100644 --- a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php +++ b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * CalDAV App - * - * @copyright 2021 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Auth; diff --git a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php index 96669558818..ed89638451e 100644 --- a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php +++ b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * CalDAV App - * - * @copyright 2021 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Auth; diff --git a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php index f7d68e4ec1d..681709cdb6f 100644 --- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php +++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php @@ -1,31 +1,14 @@ <?php + /** - * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\BirthdayCalendar; use OCA\DAV\CalDAV\BirthdayService; use OCA\DAV\CalDAV\CalendarHome; +use OCP\AppFramework\Http; use OCP\IConfig; use OCP\IUser; use Sabre\DAV\Server; @@ -43,23 +26,10 @@ class EnablePlugin extends ServerPlugin { public const NS_Nextcloud = 'http://nextcloud.com/ns'; /** - * @var IConfig - */ - protected $config; - - /** - * @var BirthdayService - */ - protected $birthdayService; - - /** * @var Server */ protected $server; - /** @var IUser */ - private $user; - /** * PublishPlugin constructor. * @@ -67,10 +37,11 @@ class EnablePlugin extends ServerPlugin { * @param BirthdayService $birthdayService * @param IUser $user */ - public function __construct(IConfig $config, BirthdayService $birthdayService, IUser $user) { - $this->config = $config; - $this->birthdayService = $birthdayService; - $this->user = $user; + public function __construct( + protected IConfig $config, + protected BirthdayService $birthdayService, + private IUser $user, + ) { } /** @@ -123,26 +94,26 @@ class EnablePlugin extends ServerPlugin { */ public function httpPost(RequestInterface $request, ResponseInterface $response) { $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); - if (!($node instanceof CalendarHome)) { + if (!$node instanceof CalendarHome) { return; } $requestBody = $request->getBodyAsString(); $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); - if ($documentType !== '{'.self::NS_Nextcloud.'}enable-birthday-calendar') { + if ($documentType !== '{' . self::NS_Nextcloud . '}enable-birthday-calendar') { return; } $owner = substr($node->getOwner(), 17); - if($owner !== $this->user->getUID()) { - $this->server->httpResponse->setStatus(403); + if ($owner !== $this->user->getUID()) { + $this->server->httpResponse->setStatus(Http::STATUS_FORBIDDEN); return false; } $this->config->setUserValue($this->user->getUID(), 'dav', 'generateBirthdayCalendar', 'yes'); $this->birthdayService->syncUser($this->user->getUID()); - $this->server->httpResponse->setStatus(204); + $this->server->httpResponse->setStatus(Http::STATUS_NO_CONTENT); return false; } diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php index 93344e33186..680b228766f 100644 --- a/apps/dav/lib/CalDAV/BirthdayService.php +++ b/apps/dav/lib/CalDAV/BirthdayService.php @@ -3,33 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Achim Königs <garfonso@tratschtante.de> - * @author Christian Weiske <cweiske@cweiske.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Sven Strickroth <email@cs-ware.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * @author Cédric Neukom <github@webguy.ch> - * - * @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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CalDAV; @@ -56,28 +32,17 @@ class BirthdayService { public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays'; public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR'; - private GroupPrincipalBackend $principalBackend; - private CalDavBackend $calDavBackEnd; - private CardDavBackend $cardDavBackEnd; - private IConfig $config; - private IDBConnection $dbConnection; - private IL10N $l10n; - /** * BirthdayService constructor. */ - public function __construct(CalDavBackend $calDavBackEnd, - CardDavBackend $cardDavBackEnd, - GroupPrincipalBackend $principalBackend, - IConfig $config, - IDBConnection $dbConnection, - IL10N $l10n) { - $this->calDavBackEnd = $calDavBackEnd; - $this->cardDavBackEnd = $cardDavBackEnd; - $this->principalBackend = $principalBackend; - $this->config = $config; - $this->dbConnection = $dbConnection; - $this->l10n = $l10n; + public function __construct( + private CalDavBackend $calDavBackEnd, + private CardDavBackend $cardDavBackEnd, + private GroupPrincipalBackend $principalBackend, + private IConfig $config, + private IDBConnection $dbConnection, + private IL10N $l10n, + ) { } public function onCardChanged(int $addressBookId, @@ -111,7 +76,7 @@ class BirthdayService { return; } foreach ($datesToSync as $type) { - $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset); + $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset); } } } @@ -132,7 +97,7 @@ class BirthdayService { $calendar = $this->ensureCalendarExists($principalUri); foreach (['', '-death', '-anniversary'] as $tag) { - $objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics'; + $objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics'; $this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true); } } @@ -163,9 +128,9 @@ class BirthdayService { * @return VCalendar|null * @throws InvalidDataException */ - public function buildDateFromContact(string $cardData, - string $dateField, - string $postfix, + public function buildDateFromContact(string $cardData, + string $dateField, + string $postfix, ?string $reminderOffset):?VCalendar { if (empty($cardData)) { return null; @@ -271,7 +236,7 @@ class BirthdayService { $vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField; $vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0'; if ($originalYear !== null) { - $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear; + $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear; } if ($reminderOffset) { $alarm = $vCal->createComponent('VALARM'); @@ -288,7 +253,7 @@ class BirthdayService { * @param string $user */ public function resetForUser(string $user):void { - $principal = 'principals/users/'.$user; + $principal = 'principals/users/' . $user; $calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); if (!$calendar) { return; // The user's birthday calendar doesn't exist, no need to purge it @@ -305,13 +270,13 @@ class BirthdayService { * @throws \Sabre\DAV\Exception\BadRequest */ public function syncUser(string $user):void { - $principal = 'principals/users/'.$user; + $principal = 'principals/users/' . $user; $this->ensureCalendarExists($principal); $books = $this->cardDavBackEnd->getAddressBooksForUser($principal); foreach ($books as $book) { $cards = $this->cardDavBackEnd->getCards($book['id']); foreach ($cards as $card) { - $this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']); + $this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']); } } } @@ -330,8 +295,8 @@ class BirthdayService { } return ( - $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() || - $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue() + $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() + || $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue() ); } diff --git a/apps/dav/lib/CalDAV/CachedSubscription.php b/apps/dav/lib/CalDAV/CachedSubscription.php index 4047f8dad0b..75ee5cb440f 100644 --- a/apps/dav/lib/CalDAV/CachedSubscription.php +++ b/apps/dav/lib/CalDAV/CachedSubscription.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -73,6 +54,11 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar { 'principal' => '{DAV:}authenticated', 'protected' => true, ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->getOwner(), + 'protected' => true, + ] ]; } @@ -97,7 +83,6 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar { 'principal' => $this->getOwner() . '/calendar-proxy-read', 'protected' => true, ], - ]; } diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php new file mode 100644 index 00000000000..cc1bab6d4fc --- /dev/null +++ b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php @@ -0,0 +1,102 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CalDAV; + +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICalendarIsEnabled; +use OCP\Calendar\ICalendarIsShared; +use OCP\Calendar\ICalendarIsWritable; +use OCP\Constants; + +class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendarIsShared, ICalendarIsWritable { + + public function __construct( + private CachedSubscription $calendar, + /** @var array<string, mixed> */ + private array $calendarInfo, + private CalDavBackend $backend, + ) { + } + + /** + * @return string defining the technical unique key + * @since 13.0.0 + */ + public function getKey(): string { + return (string)$this->calendarInfo['id']; + } + + /** + * {@inheritDoc} + */ + public function getUri(): string { + return $this->calendarInfo['uri']; + } + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * @since 13.0.0 + */ + public function getDisplayName(): ?string { + return $this->calendarInfo['{DAV:}displayname']; + } + + /** + * Calendar color + * @since 13.0.0 + */ + public function getDisplayColor(): ?string { + return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color']; + } + + public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array { + return $this->backend->search($this->calendarInfo, $pattern, $searchProperties, $options, $limit, $offset); + } + + /** + * @return int build up using \OCP\Constants + * @since 13.0.0 + */ + public function getPermissions(): int { + $permissions = $this->calendar->getACL(); + $result = 0; + foreach ($permissions as $permission) { + switch ($permission['privilege']) { + case '{DAV:}read': + $result |= Constants::PERMISSION_READ; + break; + } + } + + return $result; + } + + /** + * @since 32.0.0 + */ + public function isEnabled(): bool { + return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true; + } + + public function isWritable(): bool { + return false; + } + + public function isDeleted(): bool { + return false; + } + + public function isShared(): bool { + return true; + } + + public function getSource(): string { + return $this->calendarInfo['source']; + } +} diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionObject.php b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php index 3c1373763e1..dc9141a61b8 100644 --- a/apps/dav/lib/CalDAV/CachedSubscriptionObject.php +++ b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php new file mode 100644 index 00000000000..d64f039d05b --- /dev/null +++ b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CalDAV; + +use OCP\Calendar\ICalendarProvider; + +class CachedSubscriptionProvider implements ICalendarProvider { + + public function __construct( + private CalDavBackend $calDavBackend, + ) { + } + + public function getCalendars(string $principalUri, array $calendarUris = []): array { + $calendarInfos = $this->calDavBackend->getSubscriptionsForUser($principalUri); + + if (count($calendarUris) > 0) { + $calendarInfos = array_filter($calendarInfos, fn (array $subscription) => in_array($subscription['uri'], $calendarUris)); + } + + $calendarInfos = array_values(array_filter($calendarInfos)); + + $iCalendars = []; + foreach ($calendarInfos as $calendarInfo) { + $calendar = new CachedSubscription($this->calDavBackend, $calendarInfo); + $iCalendars[] = new CachedSubscriptionImpl( + $calendar, + $calendarInfo, + $this->calDavBackend, + ); + } + return $iCalendars; + } +} diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 1424ee4f9be..d5b0d875ede 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1,46 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2018 Georg Ehrke - * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>) - * - * @author Chih-Hsuan Yen <yan12125@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author dartcafe <github@dartcafe.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author leith abdulla <online-nextcloud@eleith.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Simon Spannagel <simonspa@kth.se> - * @author Stefan Weil <sw@weilnetz.de> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vinicius Cubas Brand <vinicius@eita.org.br> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @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: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CalDAV; use DateTime; +use DateTimeImmutable; use DateTimeInterface; +use Generator; use OCA\DAV\AppInfo\Application; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; @@ -51,12 +21,6 @@ use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectCreatedEvent; -use OCA\DAV\Events\CalendarObjectDeletedEvent; -use OCA\DAV\Events\CalendarObjectMovedEvent; -use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectRestoredEvent; -use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarPublishedEvent; use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; @@ -66,6 +30,13 @@ use OCA\DAV\Events\SubscriptionCreatedEvent; use OCA\DAV\Events\SubscriptionDeletedEvent; use OCA\DAV\Events\SubscriptionUpdatedEvent; use OCP\AppFramework\Db\TTransactional; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use OCP\Calendar\Events\CalendarObjectMovedEvent; +use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent; +use OCP\Calendar\Events\CalendarObjectRestoredEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\Calendar\Exceptions\CalendarException; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -97,6 +68,8 @@ use Sabre\VObject\ParseException; use Sabre\VObject\Property; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\MaxInstancesExceededException; +use Sabre\VObject\Recur\NoInstancesException; use function array_column; use function array_map; use function array_merge; @@ -118,6 +91,19 @@ use function time; * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php * * @package OCA\DAV\CalDAV + * + * @psalm-type CalendarInfo = array{ + * id: int, + * uri: string, + * principaluri: string, + * '{http://calendarserver.org/ns/}getctag': string, + * '{http://sabredav.org/ns}sync-token': int, + * '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet, + * '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp, + * '{DAV:}displayname': string, + * '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string, + * '{http://nextcloud.com/ns}owner-displayname': string, + * } */ class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { use TTransactional; @@ -208,7 +194,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ protected array $userDisplayNames; + private string $dbObjectsTable = 'calendarobjects'; private string $dbObjectPropertiesTable = 'calendarobjects_props'; + private string $dbObjectInvitationsTable = 'calendar_invitations'; private array $cachedObjects = []; public function __construct( @@ -225,15 +213,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * Return the number of calendars for a principal + * Return the number of calendars owned by the given principal. * - * By default this excludes the automatically generated birthday calendar + * Calendars shared with the given principal are not counted! * - * @param $principalUri - * @param bool $excludeBirthday - * @return int + * By default, this excludes the automatically generated birthday calendar. */ - public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) { + public function getCalendarsForUserCount(string $principalUri, bool $excludeBirthday = true): int { $principalUri = $this->convertPrincipal($principalUri, true); $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('*')) @@ -289,8 +275,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendars = []; while (($row = $result->fetch()) !== false) { $calendars[] = [ - 'id' => (int) $row['id'], - 'deleted_at' => (int) $row['deleted_at'], + 'id' => (int)$row['id'], + 'deleted_at' => (int)$row['deleted_at'], ]; } $result->closeCursor(); @@ -350,7 +336,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendars = []; while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -360,8 +346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), @@ -384,7 +370,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $fields = array_column($this->propertyMap, 0); $fields = array_map(function (string $field) { - return 'a.'.$field; + return 'a.' . $field; }, $fields); $fields[] = 'a.id'; $fields[] = 'a.uri'; @@ -413,19 +399,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; while ($row = $results->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; if ($row['principaluri'] === $principalUri) { continue; } - $readOnly = (int) $row['access'] === Backend::ACCESS_READ; + $readOnly = (int)$row['access'] === Backend::ACCESS_READ; if (isset($calendars[$row['id']])) { if ($readOnly) { // New share can not have more permissions than the old one. continue; } - if (isset($calendars[$row['id']][$readOnlyPropertyName]) && - $calendars[$row['id']][$readOnlyPropertyName] === 0) { + if (isset($calendars[$row['id']][$readOnlyPropertyName]) + && $calendars[$row['id']][$readOnlyPropertyName] === 0) { // Old share is already read-write, no more permissions can be gained continue; } @@ -442,8 +428,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $uri, 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), @@ -483,7 +469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $stmt = $query->executeQuery(); $calendars = []; while ($row = $stmt->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -492,8 +478,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ]; @@ -533,7 +519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->executeQuery(); while ($row = $result->fetch()) { - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; [, $name] = Uri\split($row['principaluri']); $row['displayname'] = $row['displayname'] . "($name)"; $components = []; @@ -544,8 +530,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['publicuri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint), @@ -598,7 +584,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription throw new NotFound('Node with name \'' . $uri . '\' could not be found'); } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; [, $name] = Uri\split($row['principaluri']); $row['displayname'] = $row['displayname'] . ' ' . "($name)"; $components = []; @@ -609,8 +595,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['publicuri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), @@ -653,7 +639,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -663,8 +649,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ]; @@ -677,7 +663,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null + * @psalm-return CalendarInfo|null + * @return array|null */ public function getCalendarById(int $calendarId): ?array { $fields = array_column($this->propertyMap, 0); @@ -701,7 +688,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $components = []; if ($row['components']) { $components = explode(',', $row['components']); @@ -711,7 +698,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'id' => $row['id'], 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'), '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0, '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), @@ -749,7 +736,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return null; } - $row['principaluri'] = (string) $row['principaluri']; + $row['principaluri'] = (string)$row['principaluri']; $subscription = [ 'id' => $row['id'], 'uri' => $row['uri'], @@ -757,7 +744,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'source' => $row['source'], 'lastmodified' => $row['lastmodified'], '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', + ]; + + return $this->rowToSubscription($row, $subscription); + } + + public function getSubscriptionByUri(string $principal, string $uri): ?array { + $fields = array_column($this->subscriptionPropertyMap, 0); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'synctoken'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + $query = $this->db->getQueryBuilder(); + $query->select($fields) + ->from('calendarsubscriptions') + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->setMaxResults(1); + $stmt = $query->executeQuery(); + + $row = $stmt->fetch(); + $stmt->closeCursor(); + if ($row === false) { + return null; + } + + $row['principaluri'] = (string)$row['principaluri']; + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', ]; return $this->rowToSubscription($row, $subscription); @@ -786,7 +810,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'uri' => $calendarUri, 'synctoken' => 1, 'transparent' => 0, - 'components' => 'VEVENT,VTODO', + 'components' => 'VEVENT,VTODO,VJOURNAL', 'displayname' => $calendarUri ]; @@ -805,7 +829,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; if (isset($properties[$transp])) { - $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent'); + $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent'); } foreach ($this->propertyMap as $xmlName => [$dbName, $type]) { @@ -858,7 +882,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription switch ($propertyName) { case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': $fieldName = 'transparent'; - $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); + $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent'); break; default: $fieldName = $this->propertyMap[$propertyName][0]; @@ -875,7 +899,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); $query->executeStatement(); - $this->addChanges($calendarId, [""], 2); + $this->addChanges($calendarId, [''], 2); $calendarData = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); @@ -895,7 +919,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) { - $this->atomic(function () use ($calendarId, $forceDeletePermanently) { + $this->atomic(function () use ($calendarId, $forceDeletePermanently): void { // The calendar is deleted right away if this is either enforced by the caller // or the special contacts birthday calendar or when the preference of an empty // retention (0 seconds) is set, which signals a disabled trashbin. @@ -906,6 +930,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarData = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); + $this->purgeCalendarInvitations($calendarId); + $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder(); $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable) ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId))) @@ -956,7 +982,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } public function restoreCalendar(int $id): void { - $this->atomic(function () use ($id) { + $this->atomic(function () use ($id): void { $qb = $this->db->getQueryBuilder(); $update = $qb->update('calendars') ->set('deleted_at', $qb->createNamedParameter(null)) @@ -977,6 +1003,81 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * Returns all calendar entries as a stream of data + * + * @since 32.0.0 + * + * @return Generator<array> + */ + public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator { + // extract options + $rangeStart = $options?->getRangeStart(); + $rangeCount = $options?->getRangeCount(); + // construct query + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('calendarobjects') + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->isNull('deleted_at')); + if ($rangeStart !== null) { + $qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart))); + } + if ($rangeCount !== null) { + $qb->setMaxResults($rangeCount); + } + if ($rangeStart !== null || $rangeCount !== null) { + $qb->orderBy('uid', 'ASC'); + } + $rs = $qb->executeQuery(); + // iterate through results + try { + while (($row = $rs->fetch()) !== false) { + yield $row; + } + } finally { + $rs->closeCursor(); + } + } + + /** + * Returns all calendar objects with limited metadata for a calendar + * + * Every item contains an array with the following keys: + * * id - the table row id + * * etag - An arbitrary string + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string. + * * calendardata - The iCalendar-compatible calendar data + * + * @param mixed $calendarId + * @param int $calendarType + * @return array + */ + public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array { + $query = $this->db->getQueryBuilder(); + $query->select(['id','uid', 'etag', 'uri', 'calendardata']) + ->from('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) + ->andWhere($query->expr()->isNull('deleted_at')); + $stmt = $query->executeQuery(); + + $result = []; + while (($row = $stmt->fetch()) !== false) { + $result[$row['uid']] = [ + 'id' => $row['id'], + 'etag' => $row['etag'], + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + ]; + } + $stmt->closeCursor(); + + return $result; + } + + /** * Delete all of an user's shares * * @param string $principaluri @@ -1061,12 +1162,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'uri' => $row['uri'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => (int) $row['calendarid'], - 'calendartype' => (int) $row['calendartype'], - 'size' => (int) $row['size'], + 'calendarid' => (int)$row['calendarid'], + 'calendartype' => (int)$row['calendartype'], + 'size' => (int)$row['size'], 'component' => strtolower($row['componenttype']), - 'classification' => (int) $row['classification'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + 'classification' => (int)$row['classification'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } $stmt->closeCursor(); @@ -1105,7 +1206,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'size' => (int)$row['size'], 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } $stmt->closeCursor(); @@ -1136,7 +1237,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->cachedObjects[$key]; } $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) + $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) @@ -1158,6 +1259,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return [ 'id' => $row['id'], 'uri' => $row['uri'], + 'uid' => $row['uid'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', 'calendarid' => $row['calendarid'], @@ -1165,7 +1267,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendardata' => $this->readBlob($row['calendardata']), 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'], ]; } @@ -1254,7 +1356,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) ->andWhere($qb->expr()->isNull('deleted_at')); $result = $qb->executeQuery(); - $count = (int) $result->fetchOne(); + $count = (int)$result->fetchOne(); $result->closeCursor(); if ($count !== 0) { @@ -1342,15 +1444,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) { $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') - ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) - ->set('lastmodified', $query->createNamedParameter(time())) - ->set('etag', $query->createNamedParameter($extraData['etag'])) - ->set('size', $query->createNamedParameter($extraData['size'])) - ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) - ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) - ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) - ->set('classification', $query->createNamedParameter($extraData['classification'])) - ->set('uid', $query->createNamedParameter($extraData['uid'])) + ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) + ->set('lastmodified', $query->createNamedParameter(time())) + ->set('etag', $query->createNamedParameter($extraData['etag'])) + ->set('size', $query->createNamedParameter($extraData['size'])) + ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) + ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) + ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) + ->set('classification', $query->createNamedParameter($extraData['classification'])) + ->set('uid', $query->createNamedParameter($extraData['uid'])) ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) @@ -1380,37 +1482,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * Moves a calendar object from calendar to calendar. * - * @param int $sourceCalendarId + * @param string $sourcePrincipalUri + * @param int $sourceObjectId + * @param string $targetPrincipalUri * @param int $targetCalendarId - * @param int $objectId - * @param string $oldPrincipalUri - * @param string $newPrincipalUri + * @param string $tragetObjectUri * @param int $calendarType * @return bool * @throws Exception */ - public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { + public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { $this->cachedObjects = []; - return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) { - $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); + return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) { + $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId); if (empty($object)) { return false; } + $sourceCalendarId = $object['calendarid']; + $sourceObjectUri = $object['uri']; + $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') ->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) + ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR)) + ->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) ->executeStatement(); - $this->purgeProperties($sourceCalendarId, $objectId); - $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType); + $this->purgeProperties($sourceCalendarId, $sourceObjectId); + $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType); - $this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType); - $this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType); + $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType); + $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType); - $object = $this->getCalendarObjectById($newPrincipalUri, $objectId); + $object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId); // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client if (empty($object)) { return false; @@ -1432,25 +1537,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription }, $this->db); } - - /** - * @param int $calendarObjectId - * @param int $classification - */ - public function setClassification($calendarObjectId, $classification) { - $this->cachedObjects = []; - if (!in_array($classification, [ - self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL - ])) { - throw new \InvalidArgumentException(); - } - $query = $this->db->getQueryBuilder(); - $query->update('calendarobjects') - ->set('classification', $query->createNamedParameter($classification)) - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId))) - ->executeStatement(); - } - /** * Deletes an existing calendar object. * @@ -1464,7 +1550,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) { + $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void { $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); if ($data === null) { @@ -1478,6 +1564,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->purgeProperties($calendarId, $data['id']); + $this->purgeObjectInvitations($data['uid']); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); @@ -1493,13 +1581,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription if (!empty($pathInfo['extension'])) { // Append a suffix to "free" the old URI for recreation $newUri = sprintf( - "%s-deleted.%s", + '%s-deleted.%s', $pathInfo['filename'], $pathInfo['extension'] ); } else { $newUri = sprintf( - "%s-deleted", + '%s-deleted', $pathInfo['filename'] ); } @@ -1546,9 +1634,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function restoreCalendarObject(array $objectData): void { $this->cachedObjects = []; - $this->atomic(function () use ($objectData) { - $id = (int) $objectData['id']; - $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']); + $this->atomic(function () use ($objectData): void { + $id = (int)$objectData['id']; + $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']); $targetObject = $this->getCalendarObject( $objectData['calendarid'], $restoreUri @@ -1577,17 +1665,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription // Welp, this should possibly not have happened, but let's ignore return; } - $this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']); + $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']); - $calendarRow = $this->getCalendarById((int) $row['calendarid']); + $calendarRow = $this->getCalendarById((int)$row['calendarid']); if ($calendarRow === null) { throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.'); } $this->dispatcher->dispatchTyped( new CalendarObjectRestoredEvent( - (int) $objectData['calendarid'], + (int)$objectData['calendarid'], $calendarRow, - $this->getShares((int) $row['calendarid']), + $this->getShares((int)$row['calendarid']), $row ) ); @@ -1674,7 +1762,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } } $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) + $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at']) ->from('calendarobjects') ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) @@ -1706,13 +1794,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription try { $matches = $this->validateFilterForObject($row, $filters); } catch (ParseException $ex) { - $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [ + $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [ 'app' => 'dav', 'exception' => $ex, ]); continue; } catch (InvalidDataException $ex) { - $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [ + $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [ + 'app' => 'dav', + 'exception' => $ex, + ]); + continue; + } catch (MaxInstancesExceededException $ex) { + $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [ 'app' => 'dav', 'exception' => $ex, ]); @@ -1835,7 +1929,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($compExpr) ->andWhere($propParamExpr) ->andWhere($query->expr()->iLike('i.value', - $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))) + $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%'))) ->andWhere($query->expr()->isNull('deleted_at')); if ($offset) { @@ -1877,17 +1971,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription array $searchProperties, array $options, $limit, - $offset + $offset, ) { $outerQuery = $this->db->getQueryBuilder(); $innerQuery = $this->db->getQueryBuilder(); + if (isset($calendarInfo['source'])) { + $calendarType = self::CALENDAR_TYPE_SUBSCRIPTION; + } else { + $calendarType = self::CALENDAR_TYPE_CALENDAR; + } + $innerQuery->selectDistinct('op.objectid') ->from($this->dbObjectPropertiesTable, 'op') ->andWhere($innerQuery->expr()->eq('op.calendarid', $outerQuery->createNamedParameter($calendarInfo['id']))) ->andWhere($innerQuery->expr()->eq('op.calendartype', - $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + $outerQuery->createNamedParameter($calendarType))); $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri') ->from('calendarobjects', 'c') @@ -1900,29 +2000,48 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } if (!empty($searchProperties)) { - $or = $innerQuery->expr()->orX(); + $or = []; foreach ($searchProperties as $searchProperty) { - $or->add($innerQuery->expr()->eq('op.name', - $outerQuery->createNamedParameter($searchProperty))); + $or[] = $innerQuery->expr()->eq('op.name', + $outerQuery->createNamedParameter($searchProperty)); } - $innerQuery->andWhere($or); + $innerQuery->andWhere($innerQuery->expr()->orX(...$or)); } if ($pattern !== '') { $innerQuery->andWhere($innerQuery->expr()->iLike('op.value', - $outerQuery->createNamedParameter('%' . - $this->db->escapeLikeParameter($pattern) . '%'))); + $outerQuery->createNamedParameter('%' + . $this->db->escapeLikeParameter($pattern) . '%'))); } - if (isset($options['timerange'])) { - if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) { - $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence', - $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()))); - } - if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) { - $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence', - $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()))); - } + $start = null; + $end = null; + + $hasLimit = is_int($limit); + $hasTimeRange = false; + + if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) { + /** @var DateTimeInterface $start */ + $start = $options['timerange']['start']; + $outerQuery->andWhere( + $outerQuery->expr()->gt( + 'lastoccurence', + $outerQuery->createNamedParameter($start->getTimestamp()) + ) + ); + $hasTimeRange = true; + } + + if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) { + /** @var DateTimeInterface $end */ + $end = $options['timerange']['end']; + $outerQuery->andWhere( + $outerQuery->expr()->lt( + 'firstoccurence', + $outerQuery->createNamedParameter($end->getTimestamp()) + ) + ); + $hasTimeRange = true; } if (isset($options['uid'])) { @@ -1930,64 +2049,56 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } if (!empty($options['types'])) { - $or = $outerQuery->expr()->orX(); + $or = []; foreach ($options['types'] as $type) { - $or->add($outerQuery->expr()->eq('componenttype', - $outerQuery->createNamedParameter($type))); + $or[] = $outerQuery->expr()->eq('componenttype', + $outerQuery->createNamedParameter($type)); } - $outerQuery->andWhere($or); + $outerQuery->andWhere($outerQuery->expr()->orX(...$or)); } $outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL()))); - if ($offset) { - $outerQuery->setFirstResult($offset); - } - if ($limit) { - $outerQuery->setMaxResults($limit); - } + // Without explicit order by its undefined in which order the SQL server returns the events. + // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful. + $outerQuery->addOrderBy('id'); - $result = $outerQuery->executeQuery(); - $calendarObjects = []; - while (($row = $result->fetch()) !== false) { - $start = $options['timerange']['start'] ?? null; - $end = $options['timerange']['end'] ?? null; + $offset = (int)$offset; + $outerQuery->setFirstResult($offset); - if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) { - // No filter required - $calendarObjects[] = $row; - continue; - } + $calendarObjects = []; - $isValid = $this->validateFilterForObject($row, [ - 'name' => 'VCALENDAR', - 'comp-filters' => [ - [ - 'name' => 'VEVENT', - 'comp-filters' => [], - 'prop-filters' => [], - 'is-not-defined' => false, - 'time-range' => [ - 'start' => $start, - 'end' => $end, - ], - ], - ], - 'prop-filters' => [], - 'is-not-defined' => false, - 'time-range' => null, - ]); - if (is_resource($row['calendardata'])) { - // Put the stream back to the beginning so it can be read another time - rewind($row['calendardata']); - } - if ($isValid) { - $calendarObjects[] = $row; - } + if ($hasLimit && $hasTimeRange) { + /** + * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence. + * + * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow. + * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days. + * + * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence + * and discard the events after evaluating the reoccurrence rules because they are not due within + * the next 14 days and end up with an empty result even if there are two events to show. + * + * The workaround for search requests with a limit and time range is asking for more row than requested + * and retrying if we have not reached the limit. + * + * 25 rows and 3 retries is entirely arbitrary. + */ + $maxResults = (int)max($limit, 25); + $outerQuery->setMaxResults($maxResults); + + for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) { + $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end)); + $outerQuery->setFirstResult($offset += $maxResults); + } + + $calendarObjects = array_slice($calendarObjects, 0, $limit, false); + } else { + $outerQuery->setMaxResults($limit); + $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end); } - $result->closeCursor(); - return array_map(function ($o) use ($options) { + $calendarObjects = array_map(function ($o) use ($options) { $calendarData = Reader::read($o['calendardata']); // Expand recurrences if an explicit time range is requested @@ -2023,6 +2134,72 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription }, $timezones), ]; }, $calendarObjects); + + usort($calendarObjects, function (array $a, array $b) { + /** @var DateTimeImmutable $startA */ + $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE); + /** @var DateTimeImmutable $startB */ + $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE); + + return $startA->getTimestamp() <=> $startB->getTimestamp(); + }); + + return $calendarObjects; + } + + private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array { + $calendarObjects = []; + $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface); + + $result = $query->executeQuery(); + + while (($row = $result->fetch()) !== false) { + if ($filterByTimeRange === false) { + // No filter required + $calendarObjects[] = $row; + continue; + } + + try { + $isValid = $this->validateFilterForObject($row, [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + } catch (MaxInstancesExceededException $ex) { + $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [ + 'app' => 'dav', + 'exception' => $ex, + ]); + continue; + } + + if (is_resource($row['calendardata'])) { + // Put the stream back to the beginning so it can be read another time + rewind($row['calendardata']); + } + + if ($isValid) { + $calendarObjects[] = $row; + } + } + + $result->closeCursor(); + + return $calendarObjects; } /** @@ -2100,22 +2277,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription array $componentTypes, array $searchProperties, array $searchParameters, - array $options = [] + array $options = [], ): array { return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) { $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; $calendarObjectIdQuery = $this->db->getQueryBuilder(); - $calendarOr = $calendarObjectIdQuery->expr()->orX(); - $searchOr = $calendarObjectIdQuery->expr()->orX(); + $calendarOr = []; + $searchOr = []; // Fetch calendars and subscription $calendars = $this->getCalendarsForUser($principalUri); $subscriptions = $this->getSubscriptionsForUser($principalUri); foreach ($calendars as $calendar) { - $calendarAnd = $calendarObjectIdQuery->expr()->andX(); - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id']))); - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); + $calendarAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])), + $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)), + ); // If it's shared, limit search to public events if (isset($calendar['{http://owncloud.org/ns}owner-principal']) @@ -2123,12 +2301,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); } - $calendarOr->add($calendarAnd); + $calendarOr[] = $calendarAnd; } foreach ($subscriptions as $subscription) { - $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(); - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id']))); - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); + $subscriptionAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])), + $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)), + ); // If it's shared, limit search to public events if (isset($subscription['{http://owncloud.org/ns}owner-principal']) @@ -2136,28 +2315,30 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); } - $calendarOr->add($subscriptionAnd); + $calendarOr[] = $subscriptionAnd; } foreach ($searchProperties as $property) { - $propertyAnd = $calendarObjectIdQuery->expr()->andX(); - $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); - $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter')); + $propertyAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)), + $calendarObjectIdQuery->expr()->isNull('cob.parameter'), + ); - $searchOr->add($propertyAnd); + $searchOr[] = $propertyAnd; } foreach ($searchParameters as $property => $parameter) { - $parameterAnd = $calendarObjectIdQuery->expr()->andX(); - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY))); + $parameterAnd = $calendarObjectIdQuery->expr()->andX( + $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)), + $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)), + ); - $searchOr->add($parameterAnd); + $searchOr[] = $parameterAnd; } - if ($calendarOr->count() === 0) { + if (empty($calendarOr)) { return []; } - if ($searchOr->count() === 0) { + if (empty($searchOr)) { return []; } @@ -2165,8 +2346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from($this->dbObjectPropertiesTable, 'cob') ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid')) ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY))) - ->andWhere($calendarOr) - ->andWhere($searchOr) + ->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr)) + ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr)) ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at')); if ($pattern !== '') { @@ -2201,7 +2382,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $result = $calendarObjectIdQuery->executeQuery(); $matches = []; while (($row = $result->fetch()) !== false) { - $matches[] = (int) $row['objectid']; + $matches[] = (int)$row['objectid']; } $result->closeCursor(); @@ -2288,7 +2469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendardata' => $this->readBlob($row['calendardata']), 'component' => strtolower($row['componenttype']), 'classification' => (int)$row['classification'], - 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null, + 'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null, ]; } @@ -2362,74 +2543,57 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ); $stmt = $qb->executeQuery(); $currentToken = $stmt->fetchOne(); + $initialSync = !is_numeric($syncToken); if ($currentToken === false) { return null; } - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { - $qb = $this->db->getQueryBuilder(); - - $qb->select('uri', 'operation') - ->from('calendarchanges') - ->where( - $qb->expr()->andX( - $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), - $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) - ) - )->orderBy('synctoken'); - if (is_int($limit) && $limit > 0) { - $qb->setMaxResults($limit); - } - - // Fetching all changes - $stmt = $qb->executeQuery(); - $changes = []; - - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while ($row = $stmt->fetch()) { - $changes[$row['uri']] = $row['operation']; - } - $stmt->closeCursor(); - - foreach ($changes as $uri => $operation) { - switch ($operation) { - case 1: - $result['added'][] = $uri; - break; - case 2: - $result['modified'][] = $uri; - break; - case 3: - $result['deleted'][] = $uri; - break; - } - } - } else { - // No synctoken supplied, this is the initial sync. + // evaluate if this is a initial sync and construct appropriate command + if ($initialSync) { $qb = $this->db->getQueryBuilder(); $qb->select('uri') ->from('calendarobjects') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) - ) - ); - $stmt = $qb->executeQuery(); + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->isNull('deleted_at')); + } else { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri', $qb->func()->max('operation')) + ->from('calendarchanges') + ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId))) + ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))) + ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken))) + ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken))) + ->groupBy('uri'); + } + // evaluate if limit exists + if (is_numeric($limit)) { + $qb->setMaxResults($limit); + } + // execute command + $stmt = $qb->executeQuery(); + // build results + $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []]; + // retrieve results + if ($initialSync) { $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - $stmt->closeCursor(); + } else { + // \PDO::FETCH_NUM is needed due to the inconsistent field names + // produced by doctrine for MAX() with different databases + while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) { + // assign uri (column 0) to appropriate mutation based on operation (column 1) + // forced (int) is needed as doctrine with OCI returns the operation field as string not integer + match ((int)$entry[1]) { + 1 => $result['added'][] = $entry[0], + 2 => $result['modified'][] = $entry[0], + 3 => $result['deleted'][] = $entry[0], + default => $this->logger->debug('Unknown calendar change operation detected') + }; + } } + $stmt->closeCursor(); + return $result; }, $this->db); } @@ -2492,7 +2656,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'lastmodified' => $row['lastmodified'], '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', ]; $subscriptions[] = $this->rowToSubscription($row, $subscription); @@ -2614,7 +2778,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @return void */ public function deleteSubscription($subscriptionId) { - $this->atomic(function () use ($subscriptionId) { + $this->atomic(function () use ($subscriptionId): void { $subscriptionRow = $this->getSubscriptionById($subscriptionId); $query = $this->db->getQueryBuilder(); @@ -2697,9 +2861,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function getSchedulingObjects($principalUri) { $query = $this->db->getQueryBuilder(); $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) - ->from('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->executeQuery(); + ->from('schedulingobjects') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->executeQuery(); $results = []; while (($row = $stmt->fetch()) !== false) { @@ -2727,9 +2891,47 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->cachedObjects = []; $query = $this->db->getQueryBuilder(); $query->delete('schedulingobjects') - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) - ->executeStatement(); + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) + ->executeStatement(); + } + + /** + * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection. + * + * @param int $modifiedBefore + * @param int $limit + * @return void + */ + public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void { + $query = $this->db->getQueryBuilder(); + $query->select('id') + ->from('schedulingobjects') + ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore))) + ->setMaxResults($limit); + $result = $query->executeQuery(); + $count = $result->rowCount(); + if ($count === 0) { + return; + } + $ids = array_map(static function (array $id) { + return (int)$id[0]; + }, $result->fetchAll(\PDO::FETCH_NUM)); + $result->closeCursor(); + + $numDeleted = 0; + $deleteQuery = $this->db->getQueryBuilder(); + $deleteQuery->delete('schedulingobjects') + ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)); + foreach (array_chunk($ids, 1000) as $chunk) { + $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $numDeleted += $deleteQuery->executeStatement(); + } + + if ($numDeleted === $limit) { + $this->logger->info("Deleted $limit scheduling objects, continuing with next batch"); + $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit); + } } /** @@ -2768,7 +2970,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->cachedObjects = []; $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; - $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table) { + $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void { $query = $this->db->getQueryBuilder(); $query->select('synctoken') ->from($table) @@ -2785,7 +2987,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'calendarid' => $query->createNamedParameter($calendarId), 'operation' => $query->createNamedParameter($operation), 'calendartype' => $query->createNamedParameter($calendarType), - 'created_at' => time(), + 'created_at' => $query->createNamedParameter(time()), ]); foreach ($objectUris as $uri) { $query->setParameter('uri', $uri); @@ -2803,7 +3005,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $calendarType) { + $this->atomic(function () use ($calendarId, $calendarType): void { $qbAdded = $this->db->getQueryBuilder(); $qbAdded->select('uri') ->from('calendarobjects') @@ -2834,7 +3036,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ); $resultDeleted = $qbDeleted->executeQuery(); $deletedUris = array_map(function (string $uri) { - return str_replace("-deleted.ics", ".ics", $uri); + return str_replace('-deleted.ics', '.ics', $uri); }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN)); $resultDeleted->closeCursor(); $this->addChanges($calendarId, $deletedUris, 3, $calendarType); @@ -2906,7 +3108,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $lastOccurrence = $firstOccurrence; } } else { - $it = new EventIterator($vEvents); + try { + $it = new EventIterator($vEvents); + } catch (NoInstancesException $e) { + $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ + 'app' => 'dav', + 'exception' => $e, + ]); + throw new Forbidden($e->getMessage()); + } $maxDate = new DateTime(self::MAX_DATE); $firstOccurrence = $it->getDtStart()->getTimestamp(); if ($it->isInfinite()) { @@ -2961,7 +3171,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param list<string> $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - $this->atomic(function () use ($shareable, $add, $remove) { + $this->atomic(function () use ($shareable, $add, $remove): void { $calendarId = $shareable->getResourceId(); $calendarRow = $this->getCalendarById($calendarId); if ($calendarRow === null) { @@ -2988,7 +3198,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * @param boolean $value - * @param \OCA\DAV\CalDAV\Calendar $calendar + * @param Calendar $calendar * @return string|null */ public function setPublishStatus($value, $calendar) { @@ -3023,7 +3233,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @param \OCA\DAV\CalDAV\Calendar $calendar + * @param Calendar $calendar * @return mixed */ public function getPublishStatus($calendar) { @@ -3059,7 +3269,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { $this->cachedObjects = []; - $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) { + $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void { $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); try { @@ -3100,7 +3310,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $query->setParameter('name', $property->name); $query->setParameter('parameter', null); - $query->setParameter('value', $value); + $query->setParameter('value', mb_strcut($value, 0, 254)); $query->executeStatement(); } @@ -3131,7 +3341,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * deletes all birthday calendars */ public function deleteAllBirthdayCalendars() { - $this->atomic(function () { + $this->atomic(function (): void { $query = $this->db->getQueryBuilder(); $result = $query->select(['id'])->from('calendars') ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) @@ -3151,7 +3361,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param $subscriptionId */ public function purgeAllCachedEventsForSubscription($subscriptionId) { - $this->atomic(function () use ($subscriptionId) { + $this->atomic(function () use ($subscriptionId): void { $query = $this->db->getQueryBuilder(); $query->select('uri') ->from('calendarobjects') @@ -3188,6 +3398,45 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * @param int $subscriptionId + * @param array<int> $calendarObjectIds + * @param array<string> $calendarObjectUris + */ + public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void { + if (empty($calendarObjectUris)) { + return; + } + + $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void { + foreach (array_chunk($calendarObjectIds, 1000) as $chunk) { + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY)) + ->executeStatement(); + + $query = $this->db->getQueryBuilder(); + $query->delete('calendarobjects') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY)) + ->executeStatement(); + } + + foreach (array_chunk($calendarObjectUris, 1000) as $chunk) { + $query = $this->db->getQueryBuilder(); + $query->delete('calendarchanges') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) + ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)) + ->executeStatement(); + } + $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION); + }, $this->db); + } + + /** * Move a calendar from one user to another * * @param string $uriName @@ -3270,7 +3519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->from('calendarchanges'); $result = $query->executeQuery(); - $maxId = (int) $result->fetchOne(); + $maxId = (int)$result->fetchOne(); $result->closeCursor(); if (!$maxId || $maxId < $keep) { return 0; @@ -3374,4 +3623,68 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } return $subscription; } + + /** + * delete all invitations from a given calendar + * + * @since 31.0.0 + * + * @param int $calendarId + * + * @return void + */ + protected function purgeCalendarInvitations(int $calendarId): void { + // select all calendar object uid's + $cmd = $this->db->getQueryBuilder(); + $cmd->select('uid') + ->from($this->dbObjectsTable) + ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId))); + $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + // delete all links that match object uid's + $cmd = $this->db->getQueryBuilder(); + $cmd->delete($this->dbObjectInvitationsTable) + ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY)); + foreach (array_chunk($allIds, 1000) as $chunkIds) { + $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY); + $cmd->executeStatement(); + } + } + + /** + * Delete all invitations from a given calendar event + * + * @since 31.0.0 + * + * @param string $eventId UID of the event + * + * @return void + */ + protected function purgeObjectInvitations(string $eventId): void { + $cmd = $this->db->getQueryBuilder(); + $cmd->delete($this->dbObjectInvitationsTable) + ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)); + $cmd->executeStatement(); + } + + public function unshare(IShareable $shareable, string $principal): void { + $this->atomic(function () use ($shareable, $principal): void { + $calendarData = $this->getCalendarById($shareable->getResourceId()); + if ($calendarData === null) { + throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $shareable->getResourceId()); + } + + $oldShares = $this->getShares($shareable->getResourceId()); + $unshare = $this->calendarSharingBackend->unshare($shareable, $principal); + + if ($unshare) { + $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent( + $shareable->getResourceId(), + $calendarData, + $oldShares, + [], + [$principal] + )); + } + }, $this->db); + } } diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index fbfbdf652ec..dd3a4cf3f69 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -1,30 +1,10 @@ <?php + + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Gary Kim <gary@garykim.dev> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@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\CalDAV; @@ -51,12 +31,16 @@ use Sabre\DAV\PropPatch; * @property CalDavBackend $caldavBackend */ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget { - private IConfig $config; protected IL10N $l10n; private bool $useTrashbin = true; - private LoggerInterface $logger; - public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) { + public function __construct( + BackendInterface $caldavBackend, + $calendarInfo, + IL10N $l10n, + private IConfig $config, + private LoggerInterface $logger, + ) { // Convert deletion date to ISO8601 string if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) { $calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable()) @@ -66,17 +50,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable parent::__construct($caldavBackend, $calendarInfo); - if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) { + if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) { $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays'); } - if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI && - $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) { + if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI + && $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) { $this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal'); } - - $this->config = $config; $this->l10n = $l10n; - $this->logger = $logger; } /** @@ -209,8 +190,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable $acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl); $allowedPrincipals = [ $this->getOwner(), - $this->getOwner(). '/calendar-proxy-read', - $this->getOwner(). '/calendar-proxy-write', + $this->getOwner() . '/calendar-proxy-read', + $this->getOwner() . '/calendar-proxy-write', parent::getOwner(), 'principals/system/public' ]; @@ -233,12 +214,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable } public function delete() { - if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) && - $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) { - $principal = 'principal:' . parent::getOwner(); - $this->caldavBackend->updateShares($this, [], [ - $principal - ]); + if ($this->isShared()) { + $this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI()); return; } @@ -396,7 +373,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable * @inheritDoc */ public function restore(): void { - $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']); + $this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']); } public function disableTrashbin(): void { @@ -410,9 +387,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable if (!($sourceNode instanceof CalendarObject)) { return false; } - try { - return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner()); + return $this->caldavBackend->moveCalendarObject( + $sourceNode->getOwner(), + $sourceNode->getId(), + $this->getOwner(), + $this->getResourceId(), + $targetName, + ); } catch (Exception $e) { $this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]); return false; diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php index cbf5cebc9e7..89b78ba9007 100644 --- a/apps/dav/lib/CalDAV/CalendarHome.php +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\CalDAV; @@ -30,6 +11,10 @@ use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\Integration\ExternalCalendar; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use OCA\DAV\CalDAV\Trashbin\TrashbinHome; +use OCP\App\IAppManager; +use OCP\IConfig; +use OCP\IL10N; +use OCP\Server; use Psr\Log\LoggerInterface; use Sabre\CalDAV\Backend\BackendInterface; use Sabre\CalDAV\Backend\NotificationSupport; @@ -44,31 +29,29 @@ use Sabre\DAV\MkCol; class CalendarHome extends \Sabre\CalDAV\CalendarHome { - /** @var \OCP\IL10N */ + /** @var IL10N */ private $l10n; - /** @var \OCP\IConfig */ + /** @var IConfig */ private $config; /** @var PluginManager */ private $pluginManager; - - /** @var bool */ - private $returnCachedSubscriptions = false; - - /** @var LoggerInterface */ - private $logger; private ?array $cachedChildren = null; - public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) { + public function __construct( + BackendInterface $caldavBackend, + array $principalInfo, + private LoggerInterface $logger, + private bool $returnCachedSubscriptions, + ) { parent::__construct($caldavBackend, $principalInfo); $this->l10n = \OC::$server->getL10N('dav'); - $this->config = \OC::$server->getConfig(); + $this->config = Server::get(IConfig::class); $this->pluginManager = new PluginManager( \OC::$server, - \OC::$server->getAppManager() + Server::get(IAppManager::class) ); - $this->logger = $logger; } /** @@ -166,9 +149,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { // Calendar - this covers all "regular" calendars, but not shared // only check if the method is available - if($this->caldavBackend instanceof CalDavBackend) { + if ($this->caldavBackend instanceof CalDavBackend) { $calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name); - if(!empty($calendar)) { + if (!empty($calendar)) { return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger); } } @@ -219,8 +202,4 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { $principalUri = $this->principalInfo['uri']; return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset); } - - public function enableCachedSubscriptionsForThisRequest() { - $this->returnCachedSubscriptions = true; - } } diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index 397cff38037..5f912da732e 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -3,34 +3,20 @@ declare(strict_types=1); /** - * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; +use Generator; use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCP\Calendar\CalendarExportOptions; use OCP\Calendar\Exceptions\CalendarException; +use OCP\Calendar\ICalendarExport; +use OCP\Calendar\ICalendarIsEnabled; +use OCP\Calendar\ICalendarIsShared; +use OCP\Calendar\ICalendarIsWritable; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; use OCP\Constants; @@ -44,18 +30,13 @@ use Sabre\VObject\Property; use Sabre\VObject\Reader; use function Sabre\Uri\split as uriSplit; -class CalendarImpl implements ICreateFromString, IHandleImipMessage { - private CalDavBackend $backend; - private Calendar $calendar; - /** @var array<string, mixed> */ - private array $calendarInfo; - - public function __construct(Calendar $calendar, - array $calendarInfo, - CalDavBackend $backend) { - $this->calendar = $calendar; - $this->calendarInfo = $calendarInfo; - $this->backend = $backend; +class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled { + public function __construct( + private Calendar $calendar, + /** @var array<string, mixed> */ + private array $calendarInfo, + private CalDavBackend $backend, + ) { } /** @@ -63,7 +44,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { * @since 13.0.0 */ public function getKey(): string { - return (string) $this->calendarInfo['id']; + return (string)$this->calendarInfo['id']; } /** @@ -104,7 +85,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { /** @var VCalendar $vobj */ $vobj = Reader::read($timezoneProp); $components = $vobj->getComponents(); - if(empty($components)) { + if (empty($components)) { return null; } /** @var VTimeZone $vtimezone */ @@ -112,16 +93,6 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { return $vtimezone; } - /** - * @param string $pattern which should match within the $searchProperties - * @param array $searchProperties defines the properties within the query pattern should match - * @param array $options - optional parameters: - * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]] - * @param int|null $limit - limit number of search results - * @param int|null $offset - offset for paging of search results - * @return array an array of events/journals/todos which are arrays of key-value-pairs - * @since 13.0.0 - */ public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array { return $this->backend->search($this->calendarInfo, $pattern, $searchProperties, $options, $limit, $offset); @@ -135,6 +106,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { $permissions = $this->calendar->getACL(); $result = 0; foreach ($permissions as $permission) { + if ($this->calendarInfo['principaluri'] !== $permission['principal']) { + continue; + } + switch ($permission['privilege']) { case '{DAV:}read': $result |= Constants::PERMISSION_READ; @@ -153,6 +128,20 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { } /** + * @since 32.0.0 + */ + public function isEnabled(): bool { + return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true; + } + + /** + * @since 31.0.0 + */ + public function isWritable(): bool { + return $this->calendar->canWrite(); + } + + /** * @since 26.0.0 */ public function isDeleted(): bool { @@ -160,19 +149,22 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { } /** - * Create a new calendar event for this calendar - * by way of an ICS string - * - * @param string $name the file name - needs to contain the .ics ending - * @param string $calendarData a string containing a valid VEVENT ics - * - * @throws CalendarException + * @since 31.0.0 */ - public function createFromString(string $name, string $calendarData): void { - $server = new InvitationResponseServer(false); + public function isShared(): bool { + return $this->calendar->isShared(); + } + /** + * @throws CalendarException + */ + private function createFromStringInServer( + string $name, + string $calendarData, + \OCA\DAV\Connector\Sabre\Server $server, + ): void { /** @var CustomPrincipalPlugin $plugin */ - $plugin = $server->getServer()->getPlugin('auth'); + $plugin = $server->getPlugin('auth'); // we're working around the previous implementation // that only allowed the public system principal to be used // so set the custom principal here @@ -188,14 +180,14 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { // Force calendar change URI /** @var Schedule\Plugin $schedulingPlugin */ - $schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule'); + $schedulingPlugin = $server->getPlugin('caldav-schedule'); $schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename); $stream = fopen('php://memory', 'rb+'); fwrite($stream, $calendarData); rewind($stream); try { - $server->getServer()->createFile($fullCalendarFilename, $stream); + $server->createFile($fullCalendarFilename, $stream); } catch (Conflict $e) { throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e); } finally { @@ -203,6 +195,16 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { } } + public function createFromString(string $name, string $calendarData): void { + $server = new EmbeddedCalDavServer(false); + $this->createFromStringInServer($name, $calendarData, $server->getServer()); + } + + public function createFromStringMinimal(string $name, string $calendarData): void { + $server = new InvitationResponseServer(false); + $this->createFromStringInServer($name, $calendarData, $server->getServer()); + } + /** * @throws CalendarException */ @@ -240,7 +242,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { $attendee = $vEvent->{'ATTENDEE'}->getValue(); $iTipMessage->method = $vObject->{'METHOD'}->getValue(); - if ($iTipMessage->method === 'REPLY') { + if ($iTipMessage->method === 'REQUEST') { + $iTipMessage->sender = $organizer; + $iTipMessage->recipient = $attendee; + } elseif ($iTipMessage->method === 'REPLY') { if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) { $iTipMessage->recipient = $organizer; } else { @@ -261,4 +266,27 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { public function getInvitationResponseServer(): InvitationResponseServer { return new InvitationResponseServer(false); } + + /** + * Export objects + * + * @since 32.0.0 + * + * @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed> + */ + public function export(?CalendarExportOptions $options = null): Generator { + foreach ( + $this->backend->exportCalendar( + $this->calendarInfo['id'], + $this->backend::CALENDAR_TYPE_CALENDAR, + $options + ) as $event + ) { + $vObject = Reader::read($event['calendardata']); + if ($vObject instanceof VCalendar) { + yield $vObject; + } + } + } + } diff --git a/apps/dav/lib/CalDAV/CalendarManager.php b/apps/dav/lib/CalDAV/CalendarManager.php index daa96a51392..a2d2f1cda8a 100644 --- a/apps/dav/lib/CalDAV/CalendarManager.php +++ b/apps/dav/lib/CalDAV/CalendarManager.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -31,18 +13,6 @@ use Psr\Log\LoggerInterface; class CalendarManager { - /** @var CalDavBackend */ - private $backend; - - /** @var IL10N */ - private $l10n; - - /** @var IConfig */ - private $config; - - /** @var LoggerInterface */ - private $logger; - /** * CalendarManager constructor. * @@ -50,11 +20,12 @@ class CalendarManager { * @param IL10N $l10n * @param IConfig $config */ - public function __construct(CalDavBackend $backend, IL10N $l10n, IConfig $config, LoggerInterface $logger) { - $this->backend = $backend; - $this->l10n = $l10n; - $this->config = $config; - $this->logger = $logger; + public function __construct( + private CalDavBackend $backend, + private IL10N $l10n, + private IConfig $config, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php index 0d82e5ffa76..02178b4236f 100644 --- a/apps/dav/lib/CalDAV/CalendarObject.php +++ b/apps/dav/lib/CalDAV/CalendarObject.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2017, Georg Ehrke - * @copyright Copyright (c) 2020, Gary Kim <gary@garykim.dev> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Gary Kim <gary@garykim.dev> - * @author Georg Ehrke <oc.list@georgehrke.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: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CalDAV; @@ -33,9 +14,6 @@ use Sabre\VObject\Reader; class CalendarObject extends \Sabre\CalDAV\CalendarObject { - /** @var IL10N */ - protected $l10n; - /** * CalendarObject constructor. * @@ -44,16 +22,17 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject { * @param array $calendarInfo * @param array $objectData */ - public function __construct(CalDavBackend $caldavBackend, IL10N $l10n, + public function __construct( + CalDavBackend $caldavBackend, + protected IL10N $l10n, array $calendarInfo, - array $objectData) { + array $objectData, + ) { parent::__construct($caldavBackend, $calendarInfo, $objectData); if ($this->isShared()) { unset($this->objectData['size']); } - - $this->l10n = $l10n; } /** @@ -82,7 +61,7 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject { } public function getId(): int { - return (int) $this->objectData['id']; + return (int)$this->objectData['id']; } protected function isShared() { @@ -97,19 +76,16 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject { * @param Component\VCalendar $vObject * @return void */ - private function createConfidentialObject(Component\VCalendar $vObject) { + private function createConfidentialObject(Component\VCalendar $vObject): void { /** @var Component $vElement */ - $vElement = null; - if (isset($vObject->VEVENT)) { - $vElement = $vObject->VEVENT; - } - if (isset($vObject->VJOURNAL)) { - $vElement = $vObject->VJOURNAL; - } - if (isset($vObject->VTODO)) { - $vElement = $vObject->VTODO; - } - if (!is_null($vElement)) { + $vElements = array_filter($vObject->getComponents(), static function ($vElement) { + return $vElement instanceof Component\VEvent || $vElement instanceof Component\VJournal || $vElement instanceof Component\VTodo; + }); + + foreach ($vElements as $vElement) { + if (empty($vElement->select('SUMMARY'))) { + $vElement->add('SUMMARY', $this->l10n->t('Busy')); // This is needed to mask "Untitled Event" events + } foreach ($vElement->children() as &$property) { /** @var Property $property */ switch ($property->name) { diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php index f29c601db2d..3cc4039ed36 100644 --- a/apps/dav/lib/CalDAV/CalendarProvider.php +++ b/apps/dav/lib/CalDAV/CalendarProvider.php @@ -3,28 +3,13 @@ declare(strict_types=1); /** - * @copyright 2021 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; +use OCA\DAV\Db\Property; +use OCA\DAV\Db\PropertyMapper; use OCP\Calendar\ICalendarProvider; use OCP\IConfig; use OCP\IL10N; @@ -32,39 +17,28 @@ use Psr\Log\LoggerInterface; class CalendarProvider implements ICalendarProvider { - /** @var CalDavBackend */ - private $calDavBackend; - - /** @var IL10N */ - private $l10n; - - /** @var IConfig */ - private $config; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(CalDavBackend $calDavBackend, IL10N $l10n, IConfig $config, LoggerInterface $logger) { - $this->calDavBackend = $calDavBackend; - $this->l10n = $l10n; - $this->config = $config; - $this->logger = $logger; + public function __construct( + private CalDavBackend $calDavBackend, + private IL10N $l10n, + private IConfig $config, + private LoggerInterface $logger, + private PropertyMapper $propertyMapper, + ) { } public function getCalendars(string $principalUri, array $calendarUris = []): array { - $calendarInfos = []; - if (empty($calendarUris)) { - $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri); - } else { - foreach ($calendarUris as $calendarUri) { - $calendarInfos[] = $this->calDavBackend->getCalendarByUri($principalUri, $calendarUri); - } - } - $calendarInfos = array_filter($calendarInfos); + $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri) ?? []; + + if (!empty($calendarUris)) { + $calendarInfos = array_filter($calendarInfos, function ($calendar) use ($calendarUris) { + return in_array($calendar['uri'], $calendarUris); + }); + } $iCalendars = []; foreach ($calendarInfos as $calendarInfo) { + $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri'])); $calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger); $iCalendars[] = new CalendarImpl( $calendar, @@ -74,4 +48,23 @@ class CalendarProvider implements ICalendarProvider { } return $iCalendars; } + + public function getAdditionalProperties(string $principalUri, string $calendarUri): array { + $user = str_replace('principals/users/', '', $principalUri); + $path = 'calendars/' . $user . '/' . $calendarUri; + + $properties = $this->propertyMapper->findPropertiesByPath($user, $path); + + $list = []; + foreach ($properties as $property) { + if ($property instanceof Property) { + $list[$property->getPropertyname()] = match ($property->getPropertyname()) { + '{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(), + default => $property->getPropertyvalue() + }; + } + } + + return $list; + } } diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php index 0c701d9cdcf..c0a313955bb 100644 --- a/apps/dav/lib/CalDAV/CalendarRoot.php +++ b/apps/dav/lib/CalDAV/CalendarRoot.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\CalDAV; @@ -30,25 +12,29 @@ use Sabre\CalDAV\Backend; use Sabre\DAVACL\PrincipalBackend; class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { - private LoggerInterface $logger; + private array $returnCachedSubscriptions = []; public function __construct( PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $caldavBackend, $principalPrefix, - LoggerInterface $logger + private LoggerInterface $logger, ) { parent::__construct($principalBackend, $caldavBackend, $principalPrefix); - $this->logger = $logger; } public function getChildForPrincipal(array $principal) { - return new CalendarHome($this->caldavBackend, $principal, $this->logger); + return new CalendarHome( + $this->caldavBackend, + $principal, + $this->logger, + array_key_exists($principal['uri'], $this->returnCachedSubscriptions) + ); } public function getName() { - if ($this->principalPrefix === 'principals/calendar-resources' || - $this->principalPrefix === 'principals/calendar-rooms') { + if ($this->principalPrefix === 'principals/calendar-resources' + || $this->principalPrefix === 'principals/calendar-rooms') { $parts = explode('/', $this->principalPrefix); return $parts[1]; @@ -56,4 +42,8 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { return parent::getName(); } + + public function enableReturnCachedSubscriptions(string $principalUri): void { + $this->returnCachedSubscriptions['principals/users/' . $principalUri] = true; + } } diff --git a/apps/dav/lib/CalDAV/DefaultCalendarValidator.php b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php new file mode 100644 index 00000000000..266e07ef255 --- /dev/null +++ b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use Sabre\DAV\Exception as DavException; + +class DefaultCalendarValidator { + /** + * Check if a given Calendar node is suitable to be used as the default calendar for scheduling. + * + * @throws DavException If the calendar is not suitable to be used as the default calendar + */ + public function validateScheduleDefaultCalendar(Calendar $calendar): void { + // Sanity checks for a calendar that should handle invitations + if ($calendar->isSubscription() + || !$calendar->canWrite() + || $calendar->isShared() + || $calendar->isDeleted()) { + throw new DavException('Calendar is a subscription, not writable, shared or deleted'); + } + + // Calendar must support VEVENTs + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $calendarProperties = $calendar->getProperties([$sCCS]); + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + if (!in_array('VEVENT', $supportedComponents, true)) { + throw new DavException('Calendar does not support VEVENT components'); + } + } +} diff --git a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php new file mode 100644 index 00000000000..21d8c06fa99 --- /dev/null +++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php @@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin; +use OCA\DAV\CalDAV\Publishing\PublishPlugin; +use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; +use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; +use OCA\DAV\Connector\Sabre\CachingTree; +use OCA\DAV\Connector\Sabre\DavAclPlugin; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; +use OCA\DAV\Connector\Sabre\LockPlugin; +use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCA\DAV\Events\SabrePluginAuthInitEvent; +use OCA\DAV\RootCollection; +use OCA\Theming\ThemingDefaults; +use OCP\App\IAppManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as IL10NFactory; +use OCP\Server; +use Psr\Log\LoggerInterface; + +class EmbeddedCalDavServer { + private readonly \OCA\DAV\Connector\Sabre\Server $server; + + public function __construct(bool $public = true) { + $baseUri = \OC::$WEBROOT . '/remote.php/dav/'; + $logger = Server::get(LoggerInterface::class); + $dispatcher = Server::get(IEventDispatcher::class); + $appConfig = Server::get(IAppConfig::class); + $l10nFactory = Server::get(IL10NFactory::class); + $l10n = $l10nFactory->get('dav'); + + $root = new RootCollection(); + $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root)); + + // Add maintenance plugin + $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), $l10n)); + + // Set URL explicitly due to reverse-proxy situations + $this->server->httpRequest->setUrl($baseUri); + $this->server->setBaseUri($baseUri); + + $this->server->addPlugin(new BlockLegacyClientPlugin( + Server::get(IConfig::class), + Server::get(ThemingDefaults::class), + )); + $this->server->addPlugin(new AnonymousOptionsPlugin()); + + // allow custom principal uri option + if ($public) { + $this->server->addPlugin(new PublicPrincipalPlugin()); + } else { + $this->server->addPlugin(new CustomPrincipalPlugin()); + } + + // allow setup of additional auth backends + $event = new SabrePluginAuthInitEvent($this->server); + $dispatcher->dispatchTyped($event); + + $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger)); + $this->server->addPlugin(new LockPlugin()); + $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); + + // acl + $acl = new DavAclPlugin(); + $acl->principalCollectionSet = [ + 'principals/users', 'principals/groups' + ]; + $this->server->addPlugin($acl); + + // calendar plugins + $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class))); + $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); + $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); + //$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); + $this->server->addPlugin(new PublishPlugin( + Server::get(IConfig::class), + Server::get(IURLGenerator::class) + )); + if ($appConfig->getValueString('dav', 'sendInvitations', 'yes') === 'yes') { + $this->server->addPlugin(Server::get(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); + } + + // wait with registering these until auth is handled and the filesystem is setup + $this->server->on('beforeMethod:*', function () use ($root): void { + // register plugins from apps + $pluginManager = new PluginManager( + \OC::$server, + Server::get(IAppManager::class) + ); + foreach ($pluginManager->getAppPlugins() as $appPlugin) { + $this->server->addPlugin($appPlugin); + } + foreach ($pluginManager->getAppCollections() as $appCollection) { + $root->addChild($appCollection); + } + }); + } + + public function getServer(): \OCA\DAV\Connector\Sabre\Server { + return $this->server; + } +} diff --git a/apps/dav/lib/CalDAV/EventComparisonService.php b/apps/dav/lib/CalDAV/EventComparisonService.php index 24f5da34f8e..63395e7ce1c 100644 --- a/apps/dav/lib/CalDAV/EventComparisonService.php +++ b/apps/dav/lib/CalDAV/EventComparisonService.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Anna Larch <anna.larch@gmx.net> - * - * @author 2022 Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -54,14 +37,14 @@ class EventComparisonService { */ private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool { $filterEventData = []; - foreach(self::EVENT_DIFF as $eventDiff) { + foreach (self::EVENT_DIFF as $eventDiff) { $filterEventData[] = IMipService::readPropertyWithDefault($filterEvent, $eventDiff, ''); } /** @var VEvent $component */ foreach ($eventsToFilter as $k => $eventToFilter) { $eventToFilterData = []; - foreach(self::EVENT_DIFF as $eventDiff) { + foreach (self::EVENT_DIFF as $eventDiff) { $eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, ''); } // events are identical and can be removed @@ -90,23 +73,23 @@ class EventComparisonService { $newEventComponents = $new->getComponents(); foreach ($newEventComponents as $k => $event) { - if(!$event instanceof VEvent) { + if (!$event instanceof VEvent) { unset($newEventComponents[$k]); } } - if(empty($old)) { + if (empty($old)) { return ['old' => null, 'new' => $newEventComponents]; } $oldEventComponents = $old->getComponents(); - if(is_array($oldEventComponents) && !empty($oldEventComponents)) { + if (is_array($oldEventComponents) && !empty($oldEventComponents)) { foreach ($oldEventComponents as $k => $event) { - if(!$event instanceof VEvent) { + if (!$event instanceof VEvent) { unset($oldEventComponents[$k]); continue; } - if($this->removeIfUnchanged($event, $newEventComponents)) { + if ($this->removeIfUnchanged($event, $newEventComponents)) { unset($oldEventComponents[$k]); } } diff --git a/apps/dav/lib/CalDAV/EventReader.php b/apps/dav/lib/CalDAV/EventReader.php new file mode 100644 index 00000000000..ee2b8f33f9a --- /dev/null +++ b/apps/dav/lib/CalDAV/EventReader.php @@ -0,0 +1,771 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use InvalidArgumentException; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Reader; + +class EventReader { + + protected VEvent $baseEvent; + protected DateTimeInterface $baseEventStartDate; + protected DateTimeZone $baseEventStartTimeZone; + protected DateTimeInterface $baseEventEndDate; + protected DateTimeZone $baseEventEndTimeZone; + protected bool $baseEventStartDateFloating = false; + protected bool $baseEventEndDateFloating = false; + protected int $baseEventDuration; + + protected ?EventReaderRRule $rruleIterator = null; + protected ?EventReaderRDate $rdateIterator = null; + protected ?EventReaderRRule $eruleIterator = null; + protected ?EventReaderRDate $edateIterator = null; + + protected array $recurrenceModified; + protected ?DateTimeInterface $recurrenceCurrentDate; + + protected array $dayNamesMap = [ + 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday' + ]; + protected array $monthNamesMap = [ + 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June', + 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December' + ]; + protected array $relativePositionNamesMap = [ + 1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifth', + -1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifth Last' + ]; + + /** + * Initilizes the Event Reader + * + * There is several ways to set up the iterator. + * + * 1. You can pass a VCALENDAR component (as object or string) and a UID. + * 2. You can pass an array of VEVENTs (all UIDS should match). + * 3. You can pass a single VEVENT component (as object or string). + * + * Only the second method is recommended. The other 1 and 3 will be removed + * at some point in the future. + * + * The $uid parameter is only required for the first method. + * + * @since 30.0.0 + * + * @param VCalendar|VEvent|Array|String $input + * @param string|null $uid + * @param DateTimeZone|null $timeZone reference timezone for floating dates and times + */ + public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) { + + $timeZoneFactory = new TimeZoneFactory(); + + // evaluate if the input is a string and convert it to and vobject if required + if (is_string($input)) { + $input = Reader::read($input); + } + // evaluate if input is a single event vobject and convert it to a collection + if ($input instanceof VEvent) { + $events = [$input]; + } + // evaluate if input is a calendar vobject + elseif ($input instanceof VCalendar) { + // Calendar + UID mode. + if ($uid === null) { + throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used'); + } + // extract events from calendar + $events = $input->getByUID($uid); + // evaluate if any event where found + if (count($events) === 0) { + throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid); + } + // extract calendar timezone + if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) { + $calendarTimeZone = $timeZoneFactory->fromName($input->VTIMEZONE->TZID->getValue()); + } + } + // evaluate if input is a collection of event vobjects + elseif (is_array($input)) { + $events = $input; + } else { + throw new InvalidArgumentException('Invalid input data type'); + } + // find base event instance and remove it from events collection + foreach ($events as $key => $vevent) { + if (!isset($vevent->{'RECURRENCE-ID'})) { + $this->baseEvent = $vevent; + unset($events[$key]); + } + } + + // No base event was found. CalDAV does allow cases where only + // overridden instances are stored. + // + // In this particular case, we're just going to grab the first + // event and use that instead. This may not always give the + // desired result. + if (!isset($this->baseEvent) && count($events) > 0) { + $this->baseEvent = array_shift($events); + } + + // determine the event starting time zone + // we require this to align all other dates times + // evaluate if timezone parameter was used (treat this as a override) + if ($timeZone !== null) { + $this->baseEventStartTimeZone = $timeZone; + } + // evaluate if event start date has a timezone parameter + elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) { + $this->baseEventStartTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTSTART->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC'); + } + // evaluate if event parent calendar has a time zone + elseif (isset($calendarTimeZone)) { + $this->baseEventStartTimeZone = clone $calendarTimeZone; + } + // otherwise, as a last resort use the UTC timezone + else { + $this->baseEventStartTimeZone = new DateTimeZone('UTC'); + } + + // determine the event end time zone + // we require this to align all other dates and times + // evaluate if timezone parameter was used (treat this as a override) + if ($timeZone !== null) { + $this->baseEventEndTimeZone = $timeZone; + } + // evaluate if event end date has a timezone parameter + elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) { + $this->baseEventEndTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTEND->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC'); + } + // evaluate if event parent calendar has a time zone + elseif (isset($calendarTimeZone)) { + $this->baseEventEndTimeZone = clone $calendarTimeZone; + } + // otherwise, as a last resort use the start date time zone + else { + $this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone; + } + // extract start date and time + $this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone); + $this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating(); + // determine event end date and duration + // evaluate if end date exists + // extract end date and calculate duration + if (isset($this->baseEvent->DTEND)) { + $this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone); + $this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating(); + $this->baseEventDuration + = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() + - $this->baseEventStartDate->getTimeStamp(); + } + // evaluate if duration exists + // extract duration and calculate end date + elseif (isset($this->baseEvent->DURATION)) { + $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate) + ->add($this->baseEvent->DURATION->getDateInterval()); + $this->baseEventDuration = $this->baseEventEndDate->getTimestamp() - $this->baseEventStartDate->getTimestamp(); + } + // evaluate if start date is floating + // set duration to 24 hours and calculate the end date + // according to the rfc any event without a end date or duration is a complete day + elseif ($this->baseEventStartDateFloating == true) { + $this->baseEventDuration = 86400; + $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate) + ->setTimestamp($this->baseEventStartDate->getTimestamp() + $this->baseEventDuration); + } + // otherwise, set duration to zero this should never happen + else { + $this->baseEventDuration = 0; + $this->baseEventEndDate = $this->baseEventStartDate; + } + // evaluate if RRULE exist and construct iterator + if (isset($this->baseEvent->RRULE)) { + $this->rruleIterator = new EventReaderRRule( + $this->baseEvent->RRULE->getParts(), + $this->baseEventStartDate + ); + } + // evaluate if RDATE exist and construct iterator + if (isset($this->baseEvent->RDATE)) { + $dates = []; + foreach ($this->baseEvent->RDATE as $entry) { + $dates[] = $entry->getValue(); + } + $this->rdateIterator = new EventReaderRDate( + implode(',', $dates), + $this->baseEventStartDate + ); + } + // evaluate if EXRULE exist and construct iterator + if (isset($this->baseEvent->EXRULE)) { + $this->eruleIterator = new EventReaderRRule( + $this->baseEvent->EXRULE->getParts(), + $this->baseEventStartDate + ); + } + // evaluate if EXDATE exist and construct iterator + if (isset($this->baseEvent->EXDATE)) { + $dates = []; + foreach ($this->baseEvent->EXDATE as $entry) { + $dates[] = $entry->getValue(); + } + $this->edateIterator = new EventReaderRDate( + implode(',', $dates), + $this->baseEventStartDate + ); + } + // construct collection of modified events with recurrence id as hash + foreach ($events as $vevent) { + $this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent; + } + + $this->recurrenceCurrentDate = clone $this->baseEventStartDate; + } + + /** + * retrieve date and time of event start + * + * @since 30.0.0 + * + * @return DateTime + */ + public function startDateTime(): DateTime { + return DateTime::createFromInterface($this->baseEventStartDate); + } + + /** + * retrieve time zone of event start + * + * @since 30.0.0 + * + * @return DateTimeZone + */ + public function startTimeZone(): DateTimeZone { + return $this->baseEventStartTimeZone; + } + + /** + * retrieve date and time of event end + * + * @since 30.0.0 + * + * @return DateTime + */ + public function endDateTime(): DateTime { + return DateTime::createFromInterface($this->baseEventEndDate); + } + + /** + * retrieve time zone of event end + * + * @since 30.0.0 + * + * @return DateTimeZone + */ + public function endTimeZone(): DateTimeZone { + return $this->baseEventEndTimeZone; + } + + /** + * is this an all day event + * + * @since 30.0.0 + * + * @return bool + */ + public function entireDay(): bool { + return $this->baseEventStartDateFloating; + } + + /** + * is this a recurring event + * + * @since 30.0.0 + * + * @return bool + */ + public function recurs(): bool { + return ($this->rruleIterator !== null || $this->rdateIterator !== null); + } + + /** + * event recurrence pattern + * + * @since 30.0.0 + * + * @return string|null R - Relative or A - Absolute + */ + public function recurringPattern(): ?string { + if ($this->rruleIterator === null && $this->rdateIterator === null) { + return null; + } + if ($this->rruleIterator?->isRelative()) { + return 'R'; + } + return 'A'; + } + + /** + * event recurrence precision + * + * @since 30.0.0 + * + * @return string|null daily, weekly, monthly, yearly, fixed + */ + public function recurringPrecision(): ?string { + if ($this->rruleIterator !== null) { + return $this->rruleIterator->precision(); + } + if ($this->rdateIterator !== null) { + return 'fixed'; + } + return null; + } + + /** + * event recurrence interval + * + * @since 30.0.0 + * + * @return int|null + */ + public function recurringInterval(): ?int { + return $this->rruleIterator?->interval(); + } + + /** + * event recurrence conclusion + * + * returns true if RRULE with UNTIL or COUNT (calculated) is used + * returns true RDATE is used + * returns false if RRULE or RDATE are absent, or RRRULE is infinite + * + * @since 30.0.0 + * + * @return bool + */ + public function recurringConcludes(): bool { + + // retrieve rrule conclusions + if ($this->rruleIterator?->concludesOn() !== null + || $this->rruleIterator?->concludesAfter() !== null) { + return true; + } + // retrieve rdate conclusions + if ($this->rdateIterator?->concludesAfter() !== null) { + return true; + } + + return false; + + } + + /** + * event recurrence conclusion iterations + * + * returns the COUNT value if RRULE is used + * returns the collection count if RDATE is used + * returns combined count of RRULE COUNT and RDATE if both are used + * returns null if RRULE and RDATE are absent + * + * @since 30.0.0 + * + * @return int|null + */ + public function recurringConcludesAfter(): ?int { + + // construct count place holder + $count = 0; + // retrieve and add RRULE iterations count + $count += (int)$this->rruleIterator?->concludesAfter(); + // retrieve and add RDATE iterations count + $count += (int)$this->rdateIterator?->concludesAfter(); + // return count + return !empty($count) ? $count : null; + + } + + /** + * event recurrence conclusion date + * + * returns the last date of UNTIL or COUNT (calculated) if RRULE is used + * returns the last date in the collection if RDATE is used + * returns the highest date if both RRULE and RDATE are used + * returns null if RRULE and RDATE are absent or RRULE is infinite + * + * @since 30.0.0 + * + * @return DateTime|null + */ + public function recurringConcludesOn(): ?DateTime { + + if ($this->rruleIterator !== null) { + // retrieve rrule conclusion date + $rrule = $this->rruleIterator->concludes(); + // evaluate if rrule conclusion is null + // if this is null that means the recurrence is infinate + if ($rrule === null) { + return null; + } + } + // retrieve rdate conclusion date + if ($this->rdateIterator !== null) { + $rdate = $this->rdateIterator->concludes(); + } + // evaluate if both rrule and rdate have date + if (isset($rdate) && isset($rrule)) { + // return the highest date + return (($rdate > $rrule) ? $rdate : $rrule); + } elseif (isset($rrule)) { + return $rrule; + } elseif (isset($rdate)) { + return $rdate; + } + + return null; + + } + + /** + * event recurrence days of the week + * + * returns collection of RRULE BYDAY day(s) ['MO','WE','FR'] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringDaysOfWeek(): array { + // evaluate if RRULE exists and return day(s) of the week + return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : []; + } + + /** + * event recurrence days of the week (named) + * + * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday'] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringDaysOfWeekNamed(): array { + // evaluate if RRULE exists and extract day(s) of the week + $days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : []; + // convert numberic month to month name + foreach ($days as $key => $value) { + $days[$key] = $this->dayNamesMap[$value]; + } + // return names collection + return $days; + } + + /** + * event recurrence days of the month + * + * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringDaysOfMonth(): array { + // evaluate if RRULE exists and return day(s) of the month + return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : []; + } + + /** + * event recurrence days of the year + * + * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringDaysOfYear(): array { + // evaluate if RRULE exists and return day(s) of the year + return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : []; + } + + /** + * event recurrence weeks of the month + * + * returns collection of RRULE SETPOS weeks(s) [1, 3, -1] + * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringWeeksOfMonth(): array { + // evaluate if RRULE exists and RRULE is relative return relative position(s) + return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : []; + } + + /** + * event recurrence weeks of the month (named) + * + * returns collection of RRULE SETPOS weeks(s) [1, 3, -1] + * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringWeeksOfMonthNamed(): array { + // evaluate if RRULE exists and extract relative position(s) + $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : []; + // convert numberic relative position to relative label + foreach ($positions as $key => $value) { + $positions[$key] = $this->relativePositionNamesMap[$value]; + } + // return positions collection + return $positions; + } + + /** + * event recurrence weeks of the year + * + * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringWeeksOfYear(): array { + // evaluate if RRULE exists and return weeks(s) of the year + return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : []; + } + + /** + * event recurrence months of the year + * + * returns collection of RRULE BYMONTH month(s) [3, 7, 12] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringMonthsOfYear(): array { + // evaluate if RRULE exists and return month(s) of the year + return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : []; + } + + /** + * event recurrence months of the year (named) + * + * returns collection of RRULE BYMONTH month(s) [3, 7, 12] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringMonthsOfYearNamed(): array { + // evaluate if RRULE exists and extract month(s) of the year + $months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : []; + // convert numberic month to month name + foreach ($months as $key => $value) { + $months[$key] = $this->monthNamesMap[$value]; + } + // return months collection + return $months; + } + + /** + * event recurrence relative positions + * + * returns collection of RRULE SETPOS value(s) [1, 5, -3] + * returns blank collection if RRULE is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringRelativePosition(): array { + // evaluate if RRULE exists and return relative position(s) + return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : []; + } + + /** + * event recurrence relative positions (named) + * + * returns collection of RRULE SETPOS [1, 3, -1] + * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect + * + * @since 30.0.0 + * + * @return array + */ + public function recurringRelativePositionNamed(): array { + // evaluate if RRULE exists and extract relative position(s) + $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : []; + // convert numberic relative position to relative label + foreach ($positions as $key => $value) { + $positions[$key] = $this->relativePositionNamesMap[$value]; + } + // return positions collection + return $positions; + } + + /** + * event recurrence date + * + * returns date of currently selected recurrence + * + * @since 30.0.0 + * + * @return DateTime + */ + public function recurrenceDate(): ?DateTime { + if ($this->recurrenceCurrentDate !== null) { + return DateTime::createFromInterface($this->recurrenceCurrentDate); + } else { + return null; + } + } + + /** + * event recurrence rewind + * + * sets the current recurrence to the first recurrence in the collection + * + * @since 30.0.0 + * + * @return void + */ + public function recurrenceRewind(): void { + // rewind and increment rrule + if ($this->rruleIterator !== null) { + $this->rruleIterator->rewind(); + } + // rewind and increment rdate + if ($this->rdateIterator !== null) { + $this->rdateIterator->rewind(); + } + // rewind and increment exrule + if ($this->eruleIterator !== null) { + $this->eruleIterator->rewind(); + } + // rewind and increment exdate + if ($this->edateIterator !== null) { + $this->edateIterator->rewind(); + } + // set current date to event start date + $this->recurrenceCurrentDate = clone $this->baseEventStartDate; + } + + /** + * event recurrence advance + * + * sets the current recurrence to the next recurrence in the collection + * + * @since 30.0.0 + * + * @return void + */ + public function recurrenceAdvance(): void { + // place holders + $nextOccurrenceDate = null; + $nextExceptionDate = null; + $rruleDate = null; + $rdateDate = null; + $eruleDate = null; + $edateDate = null; + // evaludate if rrule is set and advance one interation past current date + if ($this->rruleIterator !== null) { + // forward rrule to the next future date + while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) { + $this->rruleIterator->next(); + } + $rruleDate = $this->rruleIterator->current(); + } + // evaludate if rdate is set and advance one interation past current date + if ($this->rdateIterator !== null) { + // forward rdate to the next future date + while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) { + $this->rdateIterator->next(); + } + $rdateDate = $this->rdateIterator->current(); + } + if ($rruleDate !== null && $rdateDate !== null) { + $nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate; + } elseif ($rruleDate !== null) { + $nextOccurrenceDate = $rruleDate; + } elseif ($rdateDate !== null) { + $nextOccurrenceDate = $rdateDate; + } + + // evaludate if exrule is set and advance one interation past current date + if ($this->eruleIterator !== null) { + // forward exrule to the next future date + while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) { + $this->eruleIterator->next(); + } + $eruleDate = $this->eruleIterator->current(); + } + // evaludate if exdate is set and advance one interation past current date + if ($this->edateIterator !== null) { + // forward exdate to the next future date + while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) { + $this->edateIterator->next(); + } + $edateDate = $this->edateIterator->current(); + } + // evaludate if exrule and exdate are set and set nextExDate to the first next date + if ($eruleDate !== null && $edateDate !== null) { + $nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate; + } elseif ($eruleDate !== null) { + $nextExceptionDate = $eruleDate; + } elseif ($edateDate !== null) { + $nextExceptionDate = $edateDate; + } + // if the next date is part of exrule or exdate find another date + if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) { + $this->recurrenceCurrentDate = $nextOccurrenceDate; + $this->recurrenceAdvance(); + } else { + $this->recurrenceCurrentDate = $nextOccurrenceDate; + } + } + + /** + * event recurrence advance + * + * sets the current recurrence to the next recurrence in the collection after the specific date + * + * @since 30.0.0 + * + * @param DateTimeInterface $dt date and time to advance + * + * @return void + */ + public function recurrenceAdvanceTo(DateTimeInterface $dt): void { + while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) { + $this->recurrenceAdvance(); + } + } + +} diff --git a/apps/dav/lib/CalDAV/EventReaderRDate.php b/apps/dav/lib/CalDAV/EventReaderRDate.php new file mode 100644 index 00000000000..20234d06c00 --- /dev/null +++ b/apps/dav/lib/CalDAV/EventReaderRDate.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use DateTime; + +class EventReaderRDate extends \Sabre\VObject\Recur\RDateIterator { + + public function concludes(): ?DateTime { + return $this->concludesOn(); + } + + public function concludesAfter(): ?int { + return !empty($this->dates) ? count($this->dates) : null; + } + + public function concludesOn(): ?DateTime { + if (count($this->dates) > 0) { + return new DateTime( + $this->dates[array_key_last($this->dates)], + $this->startDate->getTimezone() + ); + } + + return null; + } + +} diff --git a/apps/dav/lib/CalDAV/EventReaderRRule.php b/apps/dav/lib/CalDAV/EventReaderRRule.php new file mode 100644 index 00000000000..d2b4968c479 --- /dev/null +++ b/apps/dav/lib/CalDAV/EventReaderRRule.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use DateTime; +use DateTimeInterface; + +class EventReaderRRule extends \Sabre\VObject\Recur\RRuleIterator { + + public function precision(): string { + return $this->frequency; + } + + public function interval(): int { + return $this->interval; + } + + public function concludes(): ?DateTime { + // evaluate if until value is a date + if ($this->until instanceof DateTimeInterface) { + return DateTime::createFromInterface($this->until); + } + // evaluate if count value is higher than 0 + if ($this->count > 0) { + // temporarily store current recurrence date and counter + $currentReccuranceDate = $this->currentDate; + $currentCounter = $this->counter; + // iterate over occurrences until last one (subtract 2 from count for start and end occurrence) + while ($this->counter <= ($this->count - 2)) { + $this->next(); + } + // temporarly store last reccurance date + $lastReccuranceDate = $this->currentDate; + // restore current recurrence date and counter + $this->currentDate = $currentReccuranceDate; + $this->counter = $currentCounter; + // return last recurrence date + return DateTime::createFromInterface($lastReccuranceDate); + } + + return null; + } + + public function concludesAfter(): ?int { + return !empty($this->count) ? $this->count : null; + } + + public function concludesOn(): ?DateTime { + return isset($this->until) ? DateTime::createFromInterface($this->until) : null; + } + + public function daysOfWeek(): array { + return is_array($this->byDay) ? $this->byDay : []; + } + + public function daysOfMonth(): array { + return is_array($this->byMonthDay) ? $this->byMonthDay : []; + } + + public function daysOfYear(): array { + return is_array($this->byYearDay) ? $this->byYearDay : []; + } + + public function weeksOfYear(): array { + return is_array($this->byWeekNo) ? $this->byWeekNo : []; + } + + public function monthsOfYear(): array { + return is_array($this->byMonth) ? $this->byMonth : []; + } + + public function isRelative(): bool { + return isset($this->bySetPos); + } + + public function relativePosition(): array { + return is_array($this->bySetPos) ? $this->bySetPos : []; + } + +} diff --git a/apps/dav/lib/CalDAV/Export/ExportService.php b/apps/dav/lib/CalDAV/Export/ExportService.php new file mode 100644 index 00000000000..552b9e2b675 --- /dev/null +++ b/apps/dav/lib/CalDAV/Export/ExportService.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CalDAV\Export; + +use Generator; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\ICalendarExport; +use OCP\ServerVersion; +use Sabre\VObject\Component; +use Sabre\VObject\Writer; + +/** + * Calendar Export Service + */ +class ExportService { + + public const FORMATS = ['ical', 'jcal', 'xcal']; + private string $systemVersion; + + public function __construct(ServerVersion $serverVersion) { + $this->systemVersion = $serverVersion->getVersionString(); + } + + /** + * Generates serialized content stream for a calendar and objects based in selected format + * + * @return Generator<string> + */ + public function export(ICalendarExport $calendar, CalendarExportOptions $options): Generator { + // output start of serialized content based on selected format + yield $this->exportStart($options->getFormat()); + // iterate through each returned vCalendar entry + // extract each component except timezones, convert to appropriate format and output + // extract any timezones and save them but do not output + $timezones = []; + foreach ($calendar->export($options) as $entry) { + $consecutive = false; + foreach ($entry->getComponents() as $vComponent) { + if ($vComponent->name === 'VTIMEZONE') { + if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) { + $timezones[$vComponent->TZID->getValue()] = clone $vComponent; + } + } else { + yield $this->exportObject($vComponent, $options->getFormat(), $consecutive); + $consecutive = true; + } + } + } + // iterate through each saved vTimezone entry, convert to appropriate format and output + foreach ($timezones as $vComponent) { + yield $this->exportObject($vComponent, $options->getFormat(), $consecutive); + $consecutive = true; + } + // output end of serialized content based on selected format + yield $this->exportFinish($options->getFormat()); + } + + /** + * Generates serialized content start based on selected format + */ + private function exportStart(string $format): string { + return match ($format) { + 'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar Export v' . $this->systemVersion . '\/\/EN"]],[', + 'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar Export v' . $this->systemVersion . '//EN</text></prodid></properties><components>', + default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar Export v" . $this->systemVersion . "//EN\n" + }; + } + + /** + * Generates serialized content end based on selected format + */ + private function exportFinish(string $format): string { + return match ($format) { + 'jcal' => ']]', + 'xcal' => '</components></vcalendar></icalendar>', + default => "END:VCALENDAR\n" + }; + } + + /** + * Generates serialized content for a component based on selected format + */ + private function exportObject(Component $vobject, string $format, bool $consecutive): string { + return match ($format) { + 'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject), + 'xcal' => $this->exportObjectXml($vobject), + default => Writer::write($vobject) + }; + } + + /** + * Generates serialized content for a component in xml format + */ + private function exportObjectXml(Component $vobject): string { + $writer = new \Sabre\Xml\Writer(); + $writer->openMemory(); + $writer->setIndent(false); + $vobject->xmlSerialize($writer); + return $writer->outputMemory(); + } + +} diff --git a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php index 2148e6a1da2..c2c474a90fe 100644 --- a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php +++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\FreeBusy; diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php index 627959c90f6..08dc10f7bf4 100644 --- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php +++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\ICSExportPlugin; @@ -35,18 +19,16 @@ use Sabre\VObject\Property\ICalendar\Duration; * @package OCA\DAV\CalDAV\ICSExportPlugin */ class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin { - private IConfig $config; - private LoggerInterface $logger; - /** @var string */ private const DEFAULT_REFRESH_INTERVAL = 'PT4H'; /** * ICSExportPlugin constructor. */ - public function __construct(IConfig $config, LoggerInterface $logger) { - $this->config = $config; - $this->logger = $logger; + public function __construct( + private IConfig $config, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/CalDAV/IRestorable.php b/apps/dav/lib/CalDAV/IRestorable.php index fab73c43d3a..5850e0a5645 100644 --- a/apps/dav/lib/CalDAV/IRestorable.php +++ b/apps/dav/lib/CalDAV/IRestorable.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; diff --git a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php index 0afb11c2673..acf81638679 100644 --- a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php +++ b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Integration; @@ -49,21 +33,16 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties { */ private const DELIMITER = '--'; - /** @var string */ - private $appId; - - /** @var string */ - private $calendarUri; - /** * ExternalCalendar constructor. * * @param string $appId * @param string $calendarUri */ - public function __construct(string $appId, string $calendarUri) { - $this->appId = $appId; - $this->calendarUri = $calendarUri; + public function __construct( + private string $appId, + private string $calendarUri, + ) { } /** diff --git a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php index c72112f06ba..40a8860dcb4 100644 --- a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php +++ b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Integration; diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php index e92eae2d3f1..c8a7109abde 100644 --- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php +++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php @@ -1,40 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2018, Georg Ehrke. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\InvitationResponse; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Publishing\PublishPlugin; use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; use OCA\DAV\Connector\Sabre\CachingTree; use OCA\DAV\Connector\Sabre\DavAclPlugin; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; +use OCA\DAV\Connector\Sabre\LockPlugin; +use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\RootCollection; +use OCA\Theming\ThemingDefaults; +use OCP\App\IAppManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\Server; use Psr\Log\LoggerInterface; use Sabre\VObject\ITip\Message; @@ -47,21 +38,23 @@ class InvitationResponseServer { */ public function __construct(bool $public = true) { $baseUri = \OC::$WEBROOT . '/remote.php/dav/'; - $logger = \OC::$server->get(LoggerInterface::class); - /** @var IEventDispatcher $dispatcher */ - $dispatcher = \OC::$server->query(IEventDispatcher::class); + $logger = Server::get(LoggerInterface::class); + $dispatcher = Server::get(IEventDispatcher::class); $root = new RootCollection(); $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root)); // Add maintenance plugin - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav'))); + $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav'))); // Set URL explicitly due to reverse-proxy situations $this->server->httpRequest->setUrl($baseUri); $this->server->setBaseUri($baseUri); - $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); + $this->server->addPlugin(new BlockLegacyClientPlugin( + Server::get(IConfig::class), + Server::get(ThemingDefaults::class), + )); $this->server->addPlugin(new AnonymousOptionsPlugin()); // allow custom principal uri option @@ -75,8 +68,8 @@ class InvitationResponseServer { $event = new SabrePluginAuthInitEvent($this->server); $dispatcher->dispatchTyped($event); - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger)); - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger)); + $this->server->addPlugin(new LockPlugin()); $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); // acl @@ -89,13 +82,13 @@ class InvitationResponseServer { // calendar plugins $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class))); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class))); $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); //$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin( - \OC::$server->getConfig(), - \OC::$server->getURLGenerator() + $this->server->addPlugin(new PublishPlugin( + Server::get(IConfig::class), + Server::get(IURLGenerator::class) )); // wait with registering these until auth is handled and the filesystem is setup @@ -103,7 +96,7 @@ class InvitationResponseServer { // register plugins from apps $pluginManager = new PluginManager( \OC::$server, - \OC::$server->getAppManager() + Server::get(IAppManager::class) ); foreach ($pluginManager->getAppPlugins() as $appPlugin) { $this->server->addPlugin($appPlugin); diff --git a/apps/dav/lib/CalDAV/Outbox.php b/apps/dav/lib/CalDAV/Outbox.php index eebb48e1294..608114d8093 100644 --- a/apps/dav/lib/CalDAV/Outbox.php +++ b/apps/dav/lib/CalDAV/Outbox.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -33,9 +16,6 @@ use Sabre\CalDAV\Plugin as CalDAVPlugin; */ class Outbox extends \Sabre\CalDAV\Schedule\Outbox { - /** @var IConfig */ - private $config; - /** @var null|bool */ private $disableFreeBusy = null; @@ -45,9 +25,11 @@ class Outbox extends \Sabre\CalDAV\Schedule\Outbox { * @param IConfig $config * @param string $principalUri */ - public function __construct(IConfig $config, string $principalUri) { + public function __construct( + private IConfig $config, + string $principalUri, + ) { parent::__construct($principalUri); - $this->config = $config; } /** diff --git a/apps/dav/lib/CalDAV/Plugin.php b/apps/dav/lib/CalDAV/Plugin.php index 5b367c51053..24448ae71ab 100644 --- a/apps/dav/lib/CalDAV/Plugin.php +++ b/apps/dav/lib/CalDAV/Plugin.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CalDAV; diff --git a/apps/dav/lib/CalDAV/Principal/Collection.php b/apps/dav/lib/CalDAV/Principal/Collection.php index 27997741609..b76fde66464 100644 --- a/apps/dav/lib/CalDAV/Principal/Collection.php +++ b/apps/dav/lib/CalDAV/Principal/Collection.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de> - * - * @author Christoph Seitz <christoph.seitz@posteo.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Principal; diff --git a/apps/dav/lib/CalDAV/Principal/User.php b/apps/dav/lib/CalDAV/Principal/User.php index 904ecc32e89..047d83827ed 100644 --- a/apps/dav/lib/CalDAV/Principal/User.php +++ b/apps/dav/lib/CalDAV/Principal/User.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de> - * - * @author Christoph Seitz <christoph.seitz@posteo.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Principal; diff --git a/apps/dav/lib/CalDAV/Proxy/Proxy.php b/apps/dav/lib/CalDAV/Proxy/Proxy.php index 8bafe8cc3b3..ef1ad8c634f 100644 --- a/apps/dav/lib/CalDAV/Proxy/Proxy.php +++ b/apps/dav/lib/CalDAV/Proxy/Proxy.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Proxy; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; /** * @method string getOwnerId() @@ -45,8 +29,8 @@ class Proxy extends Entity { protected $permissions; public function __construct() { - $this->addType('ownerId', 'string'); - $this->addType('proxyId', 'string'); - $this->addType('permissions', 'int'); + $this->addType('ownerId', Types::STRING); + $this->addType('proxyId', Types::STRING); + $this->addType('permissions', Types::INTEGER); } } diff --git a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php index e48e283484c..3b9b9c3d9eb 100644 --- a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php +++ b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Proxy; diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php index 16c7f86917d..9af6e544165 100644 --- a/apps/dav/lib/CalDAV/PublicCalendar.php +++ b/apps/dav/lib/CalDAV/PublicCalendar.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017, Georg Ehrke - * - * @author Gary Kim <gary@garykim.dev> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; diff --git a/apps/dav/lib/CalDAV/PublicCalendarObject.php b/apps/dav/lib/CalDAV/PublicCalendarObject.php index 69a5583d8f5..2ab40b94347 100644 --- a/apps/dav/lib/CalDAV/PublicCalendarObject.php +++ b/apps/dav/lib/CalDAV/PublicCalendarObject.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; diff --git a/apps/dav/lib/CalDAV/PublicCalendarRoot.php b/apps/dav/lib/CalDAV/PublicCalendarRoot.php index a652b6ef1e5..edfb9f8dccc 100644 --- a/apps/dav/lib/CalDAV/PublicCalendarRoot.php +++ b/apps/dav/lib/CalDAV/PublicCalendarRoot.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Thomas Citharel <nextcloud@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\CalDAV; @@ -32,18 +14,6 @@ use Sabre\DAV\Collection; class PublicCalendarRoot extends Collection { - /** @var CalDavBackend */ - protected $caldavBackend; - - /** @var \OCP\IL10N */ - protected $l10n; - - /** @var \OCP\IConfig */ - protected $config; - - /** @var LoggerInterface */ - private $logger; - /** * PublicCalendarRoot constructor. * @@ -51,12 +21,12 @@ class PublicCalendarRoot extends Collection { * @param IL10N $l10n * @param IConfig $config */ - public function __construct(CalDavBackend $caldavBackend, IL10N $l10n, - IConfig $config, LoggerInterface $logger) { - $this->caldavBackend = $caldavBackend; - $this->l10n = $l10n; - $this->config = $config; - $this->logger = $logger; + public function __construct( + protected CalDavBackend $caldavBackend, + protected IL10N $l10n, + protected IConfig $config, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php index fedd918e6a4..76378e7a1c5 100644 --- a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php +++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php @@ -1,34 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Publishing; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\Publishing\Xml\Publisher; +use OCP\AppFramework\Http; use OCP\IConfig; use OCP\IURLGenerator; use Sabre\CalDAV\Xml\Property\AllowedSharingModes; @@ -51,28 +31,21 @@ class PublishPlugin extends ServerPlugin { protected $server; /** - * Config instance to get instance secret. - * - * @var IConfig - */ - protected $config; - - /** - * URL Generator for absolute URLs. - * - * @var IURLGenerator - */ - protected $urlGenerator; - - /** * PublishPlugin constructor. * * @param IConfig $config * @param IURLGenerator $urlGenerator */ - public function __construct(IConfig $config, IURLGenerator $urlGenerator) { - $this->config = $config; - $this->urlGenerator = $urlGenerator; + public function __construct( + /** + * Config instance to get instance secret. + */ + protected IConfig $config, + /** + * URL Generator for absolute URLs. + */ + protected IURLGenerator $urlGenerator, + ) { } /** @@ -119,17 +92,17 @@ class PublishPlugin extends ServerPlugin { public function propFind(PropFind $propFind, INode $node) { if ($node instanceof Calendar) { - $propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) { + $propFind->handle('{' . self::NS_CALENDARSERVER . '}publish-url', function () use ($node) { if ($node->getPublishStatus()) { // We return the publish-url only if the calendar is published. $token = $node->getPublishStatus(); - $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token; + $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri() . 'public-calendars/') . $token; return new Publisher($publishUrl, true); } }); - $propFind->handle('{'.self::NS_CALENDARSERVER.'}allowed-sharing-modes', function () use ($node) { + $propFind->handle('{' . self::NS_CALENDARSERVER . '}allowed-sharing-modes', function () use ($node) { $canShare = (!$node->isSubscription() && $node->canWrite()); $canPublish = (!$node->isSubscription() && $node->canWrite()); @@ -155,7 +128,7 @@ class PublishPlugin extends ServerPlugin { $path = $request->getPath(); // Only handling xml - $contentType = (string) $request->getHeader('Content-Type'); + $contentType = (string)$request->getHeader('Content-Type'); if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) { return; } @@ -182,7 +155,7 @@ class PublishPlugin extends ServerPlugin { switch ($documentType) { - case '{'.self::NS_CALENDARSERVER.'}publish-calendar': + case '{' . self::NS_CALENDARSERVER . '}publish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof Calendar) { @@ -208,7 +181,7 @@ class PublishPlugin extends ServerPlugin { $node->setPublishStatus(true); // iCloud sends back the 202, so we will too. - $response->setStatus(202); + $response->setStatus(Http::STATUS_ACCEPTED); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. @@ -217,7 +190,7 @@ class PublishPlugin extends ServerPlugin { // Breaking the event chain return false; - case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar': + case '{' . self::NS_CALENDARSERVER . '}unpublish-calendar': // We can only deal with IShareableCalendar objects if (!$node instanceof Calendar) { @@ -242,7 +215,7 @@ class PublishPlugin extends ServerPlugin { $node->setPublishStatus(false); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php index cbff5e66a85..fb9b7298f9b 100644 --- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php +++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Publishing\Xml; @@ -29,22 +12,13 @@ use Sabre\Xml\XmlSerializable; class Publisher implements XmlSerializable { /** - * @var string $publishUrl - */ - protected $publishUrl; - - /** - * @var boolean $isPublished - */ - protected $isPublished; - - /** * @param string $publishUrl * @param boolean $isPublished */ - public function __construct($publishUrl, $isPublished) { - $this->publishUrl = $publishUrl; - $this->isPublished = $isPublished; + public function __construct( + protected $publishUrl, + protected $isPublished, + ) { } /** diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php index f1f5d8c4ac3..329af3a2f56 100644 --- a/apps/dav/lib/CalDAV/Reminder/Backend.php +++ b/apps/dav/lib/CalDAV/Reminder/Backend.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -38,22 +18,16 @@ use OCP\IDBConnection; */ class Backend { - /** @var IDBConnection */ - protected $db; - - /** @var ITimeFactory */ - private $timeFactory; - /** * Backend constructor. * * @param IDBConnection $db * @param ITimeFactory $timeFactory */ - public function __construct(IDBConnection $db, - ITimeFactory $timeFactory) { - $this->db = $db; - $this->timeFactory = $timeFactory; + public function __construct( + protected IDBConnection $db, + protected ITimeFactory $timeFactory, + ) { } /** @@ -64,12 +38,13 @@ class Backend { */ public function getRemindersToProcess():array { $query = $this->db->getQueryBuilder(); - $query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri']) + $query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri']) ->from('calendar_reminders', 'cr') ->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime()))) ->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id')) - ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id')); - $stmt = $query->execute(); + ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id')) + ->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri'); + $stmt = $query->executeQuery(); return array_map( [$this, 'fixRowTyping'], @@ -88,7 +63,7 @@ class Backend { $query->select('*') ->from('calendar_reminders') ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))); - $stmt = $query->execute(); + $stmt = $query->executeQuery(); return array_map( [$this, 'fixRowTyping'], @@ -141,7 +116,7 @@ class Backend { 'notification_date' => $query->createNamedParameter($notificationDate), 'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0), ]) - ->execute(); + ->executeStatement(); return $query->getLastInsertId(); } @@ -158,7 +133,7 @@ class Backend { $query->update('calendar_reminders') ->set('notification_date', $query->createNamedParameter($newNotificationDate)) ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) - ->execute(); + ->executeStatement(); } /** @@ -172,7 +147,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) - ->execute(); + ->executeStatement(); } /** @@ -185,7 +160,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))) - ->execute(); + ->executeStatement(); } /** @@ -199,7 +174,7 @@ class Backend { $query->delete('calendar_reminders') ->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId))) - ->execute(); + ->executeStatement(); } /** @@ -207,15 +182,15 @@ class Backend { * @return array */ private function fixRowTyping(array $row): array { - $row['id'] = (int) $row['id']; - $row['calendar_id'] = (int) $row['calendar_id']; - $row['object_id'] = (int) $row['object_id']; - $row['is_recurring'] = (bool) $row['is_recurring']; - $row['recurrence_id'] = (int) $row['recurrence_id']; - $row['is_recurrence_exception'] = (bool) $row['is_recurrence_exception']; - $row['is_relative'] = (bool) $row['is_relative']; - $row['notification_date'] = (int) $row['notification_date']; - $row['is_repeat_based'] = (bool) $row['is_repeat_based']; + $row['id'] = (int)$row['id']; + $row['calendar_id'] = (int)$row['calendar_id']; + $row['object_id'] = (int)$row['object_id']; + $row['is_recurring'] = (bool)$row['is_recurring']; + $row['recurrence_id'] = (int)$row['recurrence_id']; + $row['is_recurrence_exception'] = (bool)$row['is_recurrence_exception']; + $row['is_relative'] = (bool)$row['is_relative']; + $row['notification_date'] = (int)$row['notification_date']; + $row['is_repeat_based'] = (bool)$row['is_repeat_based']; return $row; } diff --git a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php index 1eb3932e611..31d60f1531d 100644 --- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -49,6 +29,6 @@ interface INotificationProvider { */ public function send(VEvent $vevent, ?string $calendarDisplayName, - array $principalEmailAddresses, + array $principalEmailAddresses, array $users = []): void; } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php index 52c994554cc..94edff98e52 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php @@ -3,30 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; @@ -51,31 +29,18 @@ abstract class AbstractProvider implements INotificationProvider { /** @var string */ public const NOTIFICATION_TYPE = ''; - protected LoggerInterface $logger; - - /** @var L10NFactory */ - protected $l10nFactory; - /** @var IL10N[] */ private $l10ns; /** @var string */ private $fallbackLanguage; - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var IConfig */ - protected $config; - - public function __construct(LoggerInterface $logger, - L10NFactory $l10nFactory, - IURLGenerator $urlGenerator, - IConfig $config) { - $this->logger = $logger; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->config = $config; + public function __construct( + protected LoggerInterface $logger, + protected L10NFactory $l10nFactory, + protected IURLGenerator $urlGenerator, + protected IConfig $config, + ) { } /** @@ -135,7 +100,7 @@ abstract class AbstractProvider implements INotificationProvider { */ private function getStatusOfEvent(VEvent $vevent):string { if ($vevent->STATUS) { - return (string) $vevent->STATUS; + return (string)$vevent->STATUS; } // Doesn't say so in the standard, diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php index 4b369b34dc0..01d51489a3b 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php index 262ceb479f0..0fd39a9e459 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php @@ -3,31 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; @@ -40,6 +17,7 @@ use OCP\L10N\IFactory as L10NFactory; use OCP\Mail\Headers\AutoSubmitted; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; +use OCP\Util; use Psr\Log\LoggerInterface; use Sabre\VObject; use Sabre\VObject\Component\VEvent; @@ -55,15 +33,14 @@ class EmailProvider extends AbstractProvider { /** @var string */ public const NOTIFICATION_TYPE = 'EMAIL'; - private IMailer $mailer; - - public function __construct(IConfig $config, - IMailer $mailer, + public function __construct( + IConfig $config, + private IMailer $mailer, LoggerInterface $logger, L10NFactory $l10nFactory, - IURLGenerator $urlGenerator) { + IURLGenerator $urlGenerator, + ) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); - $this->mailer = $mailer; } /** @@ -110,7 +87,7 @@ class EmailProvider extends AbstractProvider { $lang = $fallbackLanguage; } $l10n = $this->getL10NForLang($lang); - $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply'); + $fromEMail = Util::getDefaultEmailAddress('reminders-noreply'); $template = $this->mailer->createEMailTemplate('dav.calendarReminder'); $template->addHeader(); @@ -172,11 +149,11 @@ class EmailProvider extends AbstractProvider { $this->getAbsoluteImagePath('places/calendar.png')); if (isset($vevent->LOCATION)) { - $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'), + $template->addBodyListItem((string)$vevent->LOCATION, $l10n->t('Where:'), $this->getAbsoluteImagePath('actions/address.png')); } if (isset($vevent->DESCRIPTION)) { - $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'), + $template->addBodyListItem((string)$vevent->DESCRIPTION, $l10n->t('Description:'), $this->getAbsoluteImagePath('actions/more.png')); } } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php index 2e4f9a38493..15994bacf49 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php index 79e4e44e6d8..a3f0cce547a 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php @@ -3,30 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; @@ -52,21 +30,15 @@ class PushProvider extends AbstractProvider { /** @var string */ public const NOTIFICATION_TYPE = 'DISPLAY'; - /** @var IManager */ - private $manager; - - /** @var ITimeFactory */ - private $timeFactory; - - public function __construct(IConfig $config, - IManager $manager, + public function __construct( + IConfig $config, + private IManager $manager, LoggerInterface $logger, L10NFactory $l10nFactory, IURLGenerator $urlGenerator, - ITimeFactory $timeFactory) { + private ITimeFactory $timeFactory, + ) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); - $this->manager = $manager; - $this->timeFactory = $timeFactory; } /** @@ -87,7 +59,7 @@ class PushProvider extends AbstractProvider { } $eventDetails = $this->extractEventDetails($vevent); - $eventUUID = (string) $vevent->UID; + $eventUUID = (string)$vevent->UID; if (!$eventUUID) { return; }; @@ -122,13 +94,13 @@ class PushProvider extends AbstractProvider { return [ 'title' => isset($vevent->SUMMARY) - ? ((string) $vevent->SUMMARY) + ? ((string)$vevent->SUMMARY) : null, 'description' => isset($vevent->DESCRIPTION) - ? ((string) $vevent->DESCRIPTION) + ? ((string)$vevent->DESCRIPTION) : null, 'location' => isset($vevent->LOCATION) - ? ((string) $vevent->LOCATION) + ? ((string)$vevent->LOCATION) : null, 'all_day' => $start instanceof Property\ICalendar\Date, 'start_atom' => $start->getDateTime()->format(\DateTimeInterface::ATOM), diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php index cd8030a1177..265db09b061 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php @@ -3,30 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCP\AppFramework\QueryException; +use OCP\Server; + /** * Class NotificationProviderManager * @@ -61,7 +46,7 @@ class NotificationProviderManager { if (isset($this->providers[$type])) { return $this->providers[$type]; } - throw new NotificationProvider\ProviderNotAvailableException($type); + throw new ProviderNotAvailableException($type); } throw new NotificationTypeDoesNotExistException($type); } @@ -70,10 +55,10 @@ class NotificationProviderManager { * Registers a new provider * * @param string $providerClassName - * @throws \OCP\AppFramework\QueryException + * @throws QueryException */ public function registerProvider(string $providerClassName):void { - $provider = \OC::$server->query($providerClassName); + $provider = Server::get($providerClassName); if (!$provider instanceof INotificationProvider) { throw new \InvalidArgumentException('Invalid notification provider registered'); diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php index 16fb858bc3a..6fd2a29ede5 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php index e8f0405f3ce..137fb509f56 100644 --- a/apps/dav/lib/CalDAV/Reminder/Notifier.php +++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php @@ -3,29 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -38,6 +17,7 @@ use OCP\L10N\IFactory; use OCP\Notification\AlreadyProcessedException; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; /** * Class Notifier @@ -46,31 +26,21 @@ use OCP\Notification\INotifier; */ class Notifier implements INotifier { - /** @var IFactory */ - private $l10nFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IL10N */ private $l10n; - /** @var ITimeFactory */ - private $timeFactory; - /** * Notifier constructor. * - * @param IFactory $factory + * @param IFactory $l10nFactory * @param IURLGenerator $urlGenerator * @param ITimeFactory $timeFactory */ - public function __construct(IFactory $factory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory) { - $this->l10nFactory = $factory; - $this->urlGenerator = $urlGenerator; - $this->timeFactory = $timeFactory; + public function __construct( + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + ) { } /** @@ -99,12 +69,12 @@ class Notifier implements INotifier { * @param INotification $notification * @param string $languageCode The code of the language that should be used to prepare the notification * @return INotification - * @throws \Exception + * @throws UnknownNotificationException */ public function prepare(INotification $notification, string $languageCode):INotification { if ($notification->getApp() !== Application::APP_ID) { - throw new \InvalidArgumentException('Notification not from this app'); + throw new UnknownNotificationException('Notification not from this app'); } // Read the language from the notification @@ -116,7 +86,7 @@ class Notifier implements INotifier { return $this->prepareReminderNotification($notification); default: - throw new \InvalidArgumentException('Unknown subject'); + throw new UnknownNotificationException('Unknown subject'); } } diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index 9f4b55824e8..c75090e1560 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -3,31 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Thomas Citharel - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Reminder; @@ -55,33 +32,6 @@ use function strcasecmp; class ReminderService { - /** @var Backend */ - private $backend; - - /** @var NotificationProviderManager */ - private $notificationProviderManager; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var CalDavBackend */ - private $caldavBackend; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var IConfig */ - private $config; - - /** @var LoggerInterface */ - private $logger; - - /** @var Principal */ - private $principalConnector; - public const REMINDER_TYPE_EMAIL = 'EMAIL'; public const REMINDER_TYPE_DISPLAY = 'DISPLAY'; public const REMINDER_TYPE_AUDIO = 'AUDIO'; @@ -97,24 +47,17 @@ class ReminderService { self::REMINDER_TYPE_AUDIO ]; - public function __construct(Backend $backend, - NotificationProviderManager $notificationProviderManager, - IUserManager $userManager, - IGroupManager $groupManager, - CalDavBackend $caldavBackend, - ITimeFactory $timeFactory, - IConfig $config, - LoggerInterface $logger, - Principal $principalConnector) { - $this->backend = $backend; - $this->notificationProviderManager = $notificationProviderManager; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->caldavBackend = $caldavBackend; - $this->timeFactory = $timeFactory; - $this->config = $config; - $this->logger = $logger; - $this->principalConnector = $principalConnector; + public function __construct( + private Backend $backend, + private NotificationProviderManager $notificationProviderManager, + private IUserManager $userManager, + private IGroupManager $groupManager, + private CalDavBackend $caldavBackend, + private ITimeFactory $timeFactory, + private IConfig $config, + private LoggerInterface $logger, + private Principal $principalConnector, + ) { } /** @@ -229,14 +172,14 @@ class ReminderService { if (!$vcalendar) { return; } - $calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']); + $calendarTimeZone = $this->getCalendarTimeZone((int)$objectData['calendarid']); $vevents = $this->getAllVEventsFromVCalendar($vcalendar); if (count($vevents) === 0) { return; } - $uid = (string) $vevents[0]->UID; + $uid = (string)$vevents[0]->UID; $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $masterItem = $this->getMasterItemFromListOfVEvents($vevents); $now = $this->timeFactory->getDateTime(); @@ -306,7 +249,7 @@ class ReminderService { continue; } - if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) { + if (!\in_array((string)$valarm->ACTION, self::REMINDER_TYPES, true)) { // Action allows x-name, we don't insert reminders // into the database if they are not standard $processedAlarms[] = $alarmHash; @@ -376,7 +319,7 @@ class ReminderService { return; } - $this->backend->cleanRemindersForEvent((int) $objectData['id']); + $this->backend->cleanRemindersForEvent((int)$objectData['id']); } /** @@ -425,19 +368,19 @@ class ReminderService { $alarms[] = [ 'calendar_id' => $objectData['calendarid'], 'object_id' => $objectData['id'], - 'uid' => (string) $valarm->parent->UID, + 'uid' => (string)$valarm->parent->UID, 'is_recurring' => $isRecurring, 'recurrence_id' => $recurrenceId, 'is_recurrence_exception' => $isRecurrenceException, 'event_hash' => $eventHash, 'alarm_hash' => $alarmHash, - 'type' => (string) $valarm->ACTION, + 'type' => (string)$valarm->ACTION, 'is_relative' => $isRelative, 'notification_date' => $notificationDate->getTimestamp(), 'is_repeat_based' => false, ]; - $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0; + $repeat = isset($valarm->REPEAT) ? (int)$valarm->REPEAT->getValue() : 0; for ($i = 0; $i < $repeat; $i++) { if ($valarm->DURATION === null) { continue; @@ -447,13 +390,13 @@ class ReminderService { $alarms[] = [ 'calendar_id' => $objectData['calendarid'], 'object_id' => $objectData['id'], - 'uid' => (string) $valarm->parent->UID, + 'uid' => (string)$valarm->parent->UID, 'is_recurring' => $isRecurring, 'recurrence_id' => $recurrenceId, 'is_recurrence_exception' => $isRecurrenceException, 'event_hash' => $eventHash, 'alarm_hash' => $alarmHash, - 'type' => (string) $valarm->ACTION, + 'type' => (string)$valarm->ACTION, 'is_relative' => $isRelative, 'notification_date' => $clonedNotificationDate->getTimestamp(), 'is_repeat_based' => true, @@ -467,19 +410,26 @@ class ReminderService { * @param array $reminders */ private function writeRemindersToDatabase(array $reminders): void { + $uniqueReminders = []; foreach ($reminders as $reminder) { + $key = $reminder['notification_date'] . $reminder['event_hash'] . $reminder['type']; + if (!isset($uniqueReminders[$key])) { + $uniqueReminders[$key] = $reminder; + } + } + foreach (array_values($uniqueReminders) as $reminder) { $this->backend->insertReminder( - (int) $reminder['calendar_id'], - (int) $reminder['object_id'], + (int)$reminder['calendar_id'], + (int)$reminder['object_id'], $reminder['uid'], $reminder['is_recurring'], - (int) $reminder['recurrence_id'], + (int)$reminder['recurrence_id'], $reminder['is_recurrence_exception'], $reminder['event_hash'], $reminder['alarm_hash'], $reminder['type'], $reminder['is_relative'], - (int) $reminder['notification_date'], + (int)$reminder['notification_date'], $reminder['is_repeat_based'] ); } @@ -491,10 +441,10 @@ class ReminderService { */ private function deleteOrProcessNext(array $reminder, VObject\Component\VEvent $vevent):void { - if ($reminder['is_repeat_based'] || - !$reminder['is_recurring'] || - !$reminder['is_relative'] || - $reminder['is_recurrence_exception']) { + if ($reminder['is_repeat_based'] + || !$reminder['is_recurring'] + || !$reminder['is_relative'] + || $reminder['is_recurrence_exception']) { $this->backend->removeReminder($reminder['id']); return; } @@ -502,7 +452,7 @@ class ReminderService { $vevents = $this->getAllVEventsFromVCalendar($vevent->parent); $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $now = $this->timeFactory->getDateTime(); - $calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']); + $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']); try { $iterator = new EventIterator($vevents, $reminder['uid']); @@ -618,26 +568,26 @@ class ReminderService { */ private function getEventHash(VEvent $vevent):string { $properties = [ - (string) $vevent->DTSTART->serialize(), + (string)$vevent->DTSTART->serialize(), ]; if ($vevent->DTEND) { - $properties[] = (string) $vevent->DTEND->serialize(); + $properties[] = (string)$vevent->DTEND->serialize(); } if ($vevent->DURATION) { - $properties[] = (string) $vevent->DURATION->serialize(); + $properties[] = (string)$vevent->DURATION->serialize(); } if ($vevent->{'RECURRENCE-ID'}) { - $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize(); + $properties[] = (string)$vevent->{'RECURRENCE-ID'}->serialize(); } if ($vevent->RRULE) { - $properties[] = (string) $vevent->RRULE->serialize(); + $properties[] = (string)$vevent->RRULE->serialize(); } if ($vevent->EXDATE) { - $properties[] = (string) $vevent->EXDATE->serialize(); + $properties[] = (string)$vevent->EXDATE->serialize(); } if ($vevent->RDATE) { - $properties[] = (string) $vevent->RDATE->serialize(); + $properties[] = (string)$vevent->RDATE->serialize(); } return md5(implode('::', $properties)); @@ -652,15 +602,15 @@ class ReminderService { */ private function getAlarmHash(VAlarm $valarm):string { $properties = [ - (string) $valarm->ACTION->serialize(), - (string) $valarm->TRIGGER->serialize(), + (string)$valarm->ACTION->serialize(), + (string)$valarm->TRIGGER->serialize(), ]; if ($valarm->DURATION) { - $properties[] = (string) $valarm->DURATION->serialize(); + $properties[] = (string)$valarm->DURATION->serialize(); } if ($valarm->REPEAT) { - $properties[] = (string) $valarm->REPEAT->serialize(); + $properties[] = (string)$valarm->REPEAT->serialize(); } return md5(implode('::', $properties)); @@ -680,7 +630,7 @@ class ReminderService { return null; } - $uid = (string) $vevents[0]->UID; + $uid = (string)$vevents[0]->UID; $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); $masterItem = $this->getMasterItemFromListOfVEvents($vevents); @@ -731,7 +681,7 @@ class ReminderService { */ private function getStatusOfEvent(VEvent $vevent):string { if ($vevent->STATUS) { - return (string) $vevent->STATUS; + return (string)$vevent->STATUS; } // Doesn't say so in the standard, @@ -870,7 +820,7 @@ class ReminderService { private function getCalendarTimeZone(int $calendarid): DateTimeZone { $calendarInfo = $this->caldavBackend->getCalendarById($calendarid); $tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone'; - if (!isset($calendarInfo[$tzProp])) { + if (empty($calendarInfo[$tzProp])) { // Defaulting to UTC return new DateTimeZone('UTC'); } diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php index 92c61e72780..68bb3373346 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\ResourceBooking; @@ -45,23 +26,6 @@ use function array_values; abstract class AbstractPrincipalBackend implements BackendInterface { - /** @var IDBConnection */ - private $db; - - /** @var IUserSession */ - private $userSession; - - /** @var IGroupManager */ - private $groupManager; - - private LoggerInterface $logger; - - /** @var ProxyMapper */ - private $proxyMapper; - - /** @var string */ - private $principalPrefix; - /** @var string */ private $dbTableName; @@ -71,27 +35,19 @@ abstract class AbstractPrincipalBackend implements BackendInterface { /** @var string */ private $dbForeignKeyName; - /** @var string */ - private $cuType; - - public function __construct(IDBConnection $dbConnection, - IUserSession $userSession, - IGroupManager $groupManager, - LoggerInterface $logger, - ProxyMapper $proxyMapper, - string $principalPrefix, + public function __construct( + private IDBConnection $db, + private IUserSession $userSession, + private IGroupManager $groupManager, + private LoggerInterface $logger, + private ProxyMapper $proxyMapper, + private string $principalPrefix, string $dbPrefix, - string $cuType) { - $this->db = $dbConnection; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->logger = $logger; - $this->proxyMapper = $proxyMapper; - $this->principalPrefix = $principalPrefix; + private string $cuType, + ) { $this->dbTableName = 'calendar_' . $dbPrefix . 's'; $this->dbMetaDataTableName = $this->dbTableName . '_md'; $this->dbForeignKeyName = $dbPrefix . '_id'; - $this->cuType = $cuType; } use PrincipalProxyTrait; @@ -130,8 +86,8 @@ abstract class AbstractPrincipalBackend implements BackendInterface { $metaDataById[$metaDataRow[$this->dbForeignKeyName]] = []; } - $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] = - $metaDataRow['value']; + $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] + = $metaDataRow['value']; } while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { @@ -406,7 +362,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface { try { $stmt = $query->executeQuery(); } catch (Exception $e) { - $this->logger->error("Could not search resources: " . $e->getMessage(), ['exception' => $e]); + $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]); } $rows = []; @@ -515,9 +471,9 @@ abstract class AbstractPrincipalBackend implements BackendInterface { * @return bool */ private function isAllowedToAccessResource(array $row, array $userGroups): bool { - if (!isset($row['group_restrictions']) || - $row['group_restrictions'] === null || - $row['group_restrictions'] === '') { + if (!isset($row['group_restrictions']) + || $row['group_restrictions'] === null + || $row['group_restrictions'] === '') { return true; } diff --git a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php index 45795377e11..c70d93daf52 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\ResourceBooking; diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php index 829ca7d6033..5704b23ae14 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\ResourceBooking; diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php index 9a43d5bdc91..399d1a46639 100644 --- a/apps/dav/lib/CalDAV/RetentionService.php +++ b/apps/dav/lib/CalDAV/RetentionService.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -34,29 +17,19 @@ class RetentionService { public const RETENTION_CONFIG_KEY = 'calendarRetentionObligation'; private const DEFAULT_RETENTION_SECONDS = 30 * 24 * 60 * 60; - /** @var IConfig */ - private $config; - - /** @var ITimeFactory */ - private $time; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(IConfig $config, - ITimeFactory $time, - CalDavBackend $calDavBackend) { - $this->config = $config; - $this->time = $time; - $this->calDavBackend = $calDavBackend; + public function __construct( + private IConfig $config, + private ITimeFactory $time, + private CalDavBackend $calDavBackend, + ) { } public function getDuration(): int { return max( - (int) $this->config->getAppValue( + (int)$this->config->getAppValue( Application::APP_ID, self::RETENTION_CONFIG_KEY, - (string) self::DEFAULT_RETENTION_SECONDS + (string)self::DEFAULT_RETENTION_SECONDS ), 0 // Just making sure we don't delete things in the future when a negative number is passed ); diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index fcc2ae1e166..2af6b162d8d 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -1,38 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2017, Georg Ehrke - * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). - * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). - * @copyright 2022 Anna Larch <anna.larch@gmx.net> - * - * @author brad2014 <brad2014@users.noreply.github.com> - * @author Brad Rubenstein <brad@wbr.tech> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Leon Klingele <leon@struktur.de> - * @author Nick Sweeting <git@sweeting.me> - * @author rakekniven <mark.ziegler@rakekniven.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @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-FileCopyrightText: 2007-2015 fruux GmbH (https://fruux.com/) + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CalDAV\Schedule; @@ -40,9 +12,13 @@ use OCA\DAV\CalDAV\CalendarObject; use OCA\DAV\CalDAV\EventComparisonService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; -use OCP\IConfig; -use OCP\IUserManager; +use OCP\IAppConfig; +use OCP\IUserSession; use OCP\Mail\IMailer; +use OCP\Mail\Provider\Address; +use OCP\Mail\Provider\Attachment; +use OCP\Mail\Provider\IManager as IMailManager; +use OCP\Mail\Provider\IMessageSend; use OCP\Util; use Psr\Log\LoggerInterface; use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin; @@ -69,41 +45,26 @@ use Sabre\VObject\Reader; * @license http://sabre.io/license/ Modified BSD License */ class IMipPlugin extends SabreIMipPlugin { - private ?string $userId; - private IConfig $config; - private IMailer $mailer; - private LoggerInterface $logger; - private ITimeFactory $timeFactory; - private Defaults $defaults; - private IUserManager $userManager; + private ?VCalendar $vCalendar = null; - private IMipService $imipService; public const MAX_DATE = '2038-01-01'; public const METHOD_REQUEST = 'request'; public const METHOD_REPLY = 'reply'; public const METHOD_CANCEL = 'cancel'; - public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages - private EventComparisonService $eventComparisonService; - - public function __construct(IConfig $config, - IMailer $mailer, - LoggerInterface $logger, - ITimeFactory $timeFactory, - Defaults $defaults, - IUserManager $userManager, - $userId, - IMipService $imipService, - EventComparisonService $eventComparisonService) { + public const IMIP_INDENT = 15; + + public function __construct( + private IAppConfig $config, + private IMailer $mailer, + private LoggerInterface $logger, + private ITimeFactory $timeFactory, + private Defaults $defaults, + private IUserSession $userSession, + private IMipService $imipService, + private EventComparisonService $eventComparisonService, + private IMailManager $mailManager, + ) { parent::__construct(''); - $this->userId = $userId; - $this->config = $config; - $this->mailer = $mailer; - $this->logger = $logger; - $this->timeFactory = $timeFactory; - $this->defaults = $defaults; - $this->userManager = $userManager; - $this->imipService = $imipService; - $this->eventComparisonService = $eventComparisonService; } public function initialize(DAV\Server $server): void { @@ -120,7 +81,7 @@ class IMipPlugin extends SabreIMipPlugin { * @param bool $modified modified */ public function beforeWriteContent($uri, INode $node, $data, $modified): void { - if(!$node instanceof CalendarObject) { + if (!$node instanceof CalendarObject) { return; } /** @var VCalendar $vCalendar */ @@ -135,8 +96,8 @@ class IMipPlugin extends SabreIMipPlugin { * @return void */ public function schedule(Message $iTipMessage) { - // Not sending any emails if the system considers the update - // insignificant. + + // Not sending any emails if the system considers the update insignificant if (!$iTipMessage->significantChange) { if (!$iTipMessage->scheduleStatus) { $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; @@ -177,7 +138,7 @@ class IMipPlugin extends SabreIMipPlugin { // No changed events after all - this shouldn't happen if there is significant change yet here we are // The scheduling status is debatable - if(empty($vEvent)) { + if (empty($vEvent)) { $this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents'); $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; return; @@ -189,15 +150,16 @@ class IMipPlugin extends SabreIMipPlugin { // we also might not have an old event as this could be a new // invitation, or a new recurrence exception $attendee = $this->imipService->getCurrentAttendee($iTipMessage); - if($attendee === null) { + if ($attendee === null) { $uid = $vEvent->UID ?? 'no UID found'; $this->logger->debug('Could not find recipient ' . $recipient . ' as attendee for event with UID ' . $uid); $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; return; } - // Don't send emails to things - if($this->imipService->isRoomOrResource($attendee)) { - $this->logger->debug('No invitation sent as recipient is room or resource', [ + // Don't send emails to rooms, resources and circles + if ($this->imipService->isRoomOrResource($attendee) + || $this->imipService->isCircle($attendee)) { + $this->logger->debug('No invitation sent as recipient is room, resource or circle', [ 'attendee' => $recipient, ]); $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; @@ -206,17 +168,16 @@ class IMipPlugin extends SabreIMipPlugin { $this->imipService->setL10n($attendee); // Build the sender name. - // Due to a bug in sabre, the senderName property for an iTIP message - // can actually also be a VObject Property - /** @var Parameter|string|null $senderName */ - $senderName = $iTipMessage->senderName ?: null; - if($senderName instanceof Parameter) { - $senderName = $senderName->getValue() ?? null; - } - - // Try to get the sender name from the current user id if available. - if ($this->userId !== null && ($senderName === null || empty(trim($senderName)))) { - $senderName = $this->userManager->getDisplayName($this->userId); + // Due to a bug in sabre, the senderName property for an iTIP message can actually also be a VObject Property + // If the iTIP message senderName is null or empty use the user session name as the senderName + if (($iTipMessage->senderName instanceof Parameter) && !empty(trim($iTipMessage->senderName->getValue()))) { + $senderName = trim($iTipMessage->senderName->getValue()); + } elseif (is_string($iTipMessage->senderName) && !empty(trim($iTipMessage->senderName))) { + $senderName = trim($iTipMessage->senderName); + } elseif ($this->userSession->getUser() !== null) { + $senderName = trim($this->userSession->getUser()->getDisplayName()); + } else { + $senderName = ''; } $sender = substr($iTipMessage->sender, 7); @@ -225,7 +186,7 @@ class IMipPlugin extends SabreIMipPlugin { switch (strtolower($iTipMessage->method)) { case self::METHOD_REPLY: $method = self::METHOD_REPLY; - $data = $this->imipService->buildBodyData($vEvent, $oldVevent); + $data = $this->imipService->buildReplyBodyData($vEvent); $replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage); break; case self::METHOD_CANCEL: @@ -244,21 +205,6 @@ class IMipPlugin extends SabreIMipPlugin { $fromEMail = Util::getDefaultEmailAddress('invitations-noreply'); $fromName = $this->imipService->getFrom($senderName, $this->defaults->getName()); - $message = $this->mailer->createMessage() - ->setFrom([$fromEMail => $fromName]); - - if ($recipientName !== null) { - $message->setTo([$recipient => $recipientName]); - } else { - $message->setTo([$recipient]); - } - - if ($senderName !== null) { - $message->setReplyTo([$sender => $senderName]); - } else { - $message->setReplyTo([$sender]); - } - $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data); $template->addHeader(); @@ -288,7 +234,7 @@ class IMipPlugin extends SabreIMipPlugin { */ $recipientDomain = substr(strrchr($recipient, '@'), 1); - $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes')))); + $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getValueString('dav', 'invitation_link_recipients', 'yes')))); if (strcmp('yes', $invitationLinkRecipients[0]) === 0 || in_array(strtolower($recipient), $invitationLinkRecipients) @@ -300,18 +246,70 @@ class IMipPlugin extends SabreIMipPlugin { } $template->addFooter(); - - $message->useTemplate($template); - + // convert iTip Message to string $itip_msg = $iTipMessage->message->serialize(); - $message->attachInline( - $itip_msg, - 'event.ics', - 'text/calendar; method=' . $iTipMessage->method, - ); + + $mailService = null; try { - $failed = $this->mailer->send($message); + if ($this->config->getValueBool('core', 'mail_providers_enabled', true)) { + // retrieve user object + $user = $this->userSession->getUser(); + if ($user !== null) { + // retrieve appropriate service with the same address as sender + $mailService = $this->mailManager->findServiceByAddress($user->getUID(), $sender); + } + } + + // The display name in Nextcloud can use utf-8. + // As the default charset for text/* is us-ascii, it's important to explicitly define it. + // See https://www.rfc-editor.org/rfc/rfc6047.html#section-2.4. + $contentType = 'text/calendar; method=' . $iTipMessage->method . '; charset="utf-8"'; + + // evaluate if a mail service was found and has sending capabilities + if ($mailService instanceof IMessageSend) { + // construct mail message and set required parameters + $message = $mailService->initiateMessage(); + $message->setFrom( + (new Address($sender, $fromName)) + ); + $message->setTo( + (new Address($recipient, $recipientName)) + ); + $message->setSubject($template->renderSubject()); + $message->setBodyPlain($template->renderText()); + $message->setBodyHtml($template->renderHtml()); + // Adding name=event.ics is a trick to make the invitation also appear + // as a file attachment in mail clients like Thunderbird or Evolution. + $message->setAttachments((new Attachment( + $itip_msg, + null, + $contentType . '; name=event.ics', + true + ))); + // send message + $mailService->sendMessage($message); + } else { + // construct symfony mailer message and set required parameters + $message = $this->mailer->createMessage(); + $message->setFrom([$fromEMail => $fromName]); + $message->setTo( + (($recipientName !== null) ? [$recipient => $recipientName] : [$recipient]) + ); + $message->setReplyTo( + (($senderName !== null) ? [$sender => $senderName] : [$sender]) + ); + $message->useTemplate($template); + // Using a different content type because Symfony Mailer/Mime will append the name to + // the content type header and attachInline does not allow null. + $message->attachInline( + $itip_msg, + 'event.ics', + $contentType, + ); + $failed = $this->mailer->send($message); + } + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; if (!empty($failed)) { $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index 4cd859d79ac..54c0bc31849 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -1,31 +1,16 @@ <?php declare(strict_types=1); -/* - * DAV App - * - * @copyright 2022 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Schedule; use OC\URLGenerator; +use OCA\DAV\CalDAV\EventReader; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; @@ -42,11 +27,6 @@ use Sabre\VObject\Recur\EventIterator; class IMipService { - private URLGenerator $urlGenerator; - private IConfig $config; - private IDBConnection $db; - private ISecureRandom $random; - private L10NFactory $l10nFactory; private IL10N $l10n; /** @var string[] */ @@ -57,18 +37,17 @@ class IMipService { 'meeting_location' => 'LOCATION' ]; - public function __construct(URLGenerator $urlGenerator, - IConfig $config, - IDBConnection $db, - ISecureRandom $random, - L10NFactory $l10nFactory) { - $this->urlGenerator = $urlGenerator; - $this->config = $config; - $this->db = $db; - $this->random = $random; - $this->l10nFactory = $l10nFactory; - $default = $this->l10nFactory->findGenericLanguage(); - $this->l10n = $this->l10nFactory->get('dav', $default); + public function __construct( + private URLGenerator $urlGenerator, + private IConfig $config, + private IDBConnection $db, + private ISecureRandom $random, + private L10NFactory $l10nFactory, + private ITimeFactory $timeFactory, + ) { + $language = $this->l10nFactory->findGenericLanguage(); + $locale = $this->l10nFactory->findLocale($language); + $this->l10n = $this->l10nFactory->get('dav', $language, $locale); } /** @@ -100,7 +79,7 @@ class IMipService { return $default; } $newstring = $vevent->$property->getValue(); - if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) { + if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) { $oldstring = $oldVEvent->$property->getValue(); return sprintf($strikethrough, $oldstring, $newstring); } @@ -147,11 +126,15 @@ class IMipService { * @return array */ public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array { + + // construct event reader + $eventReaderCurrent = new EventReader($vEvent); + $eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null; $defaultVal = ''; $data = []; - $data['meeting_when'] = $this->generateWhenString($vEvent); + $data['meeting_when'] = $this->generateWhenString($eventReaderCurrent); - foreach(self::STRING_DIFF as $key => $property) { + foreach (self::STRING_DIFF as $key => $property) { $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal); } @@ -161,8 +144,8 @@ class IMipService { $data['meeting_location_html'] = $locationHtml; } - if(!empty($oldVEvent)) { - $oldMeetingWhen = $this->generateWhenString($oldVEvent); + if (!empty($oldVEvent)) { + $oldMeetingWhen = $this->generateWhenString($eventReaderPrevious); $data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']); $data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']); $data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']); @@ -170,107 +153,635 @@ class IMipService { $oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal); $data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url']; - $data['meeting_when_html'] = - ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null) - ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) - : $data['meeting_when']; + $data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when']; + } + // generate occurring next string + if ($eventReaderCurrent->recurs()) { + $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent); } return $data; } /** - * @param IL10N $this->l10n - * @param VEvent $vevent - * @return false|int|string + * @param VEvent $vEvent + * @return array */ - public function generateWhenString(VEvent $vevent) { - /** @var Property\ICalendar\DateTime $dtstart */ - $dtstart = $vevent->DTSTART; - if (isset($vevent->DTEND)) { - /** @var Property\ICalendar\DateTime $dtend */ - $dtend = $vevent->DTEND; - } elseif (isset($vevent->DURATION)) { - $isFloating = $dtstart->isFloating(); - $dtend = clone $dtstart; - $endDateTime = $dtend->getDateTime(); - $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); - $dtend->setDateTime($endDateTime, $isFloating); - } elseif (!$dtstart->hasTime()) { - $isFloating = $dtstart->isFloating(); - $dtend = clone $dtstart; - $endDateTime = $dtend->getDateTime(); - $endDateTime = $endDateTime->modify('+1 day'); - $dtend->setDateTime($endDateTime, $isFloating); - } else { - $dtend = clone $dtstart; + public function buildReplyBodyData(VEvent $vEvent): array { + // construct event reader + $eventReader = new EventReader($vEvent); + $defaultVal = ''; + $data = []; + $data['meeting_when'] = $this->generateWhenString($eventReader); + + foreach (self::STRING_DIFF as $key => $property) { + $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal); } - /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ - /** @var \DateTimeImmutable $dtstartDt */ - $dtstartDt = $dtstart->getDateTime(); + if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) { + $data['meeting_location_html'] = $locationHtml; + } - /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */ - /** @var \DateTimeImmutable $dtendDt */ - $dtendDt = $dtend->getDateTime(); + $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : ''; - $diff = $dtstartDt->diff($dtendDt); + // generate occurring next string + if ($eventReader->recurs()) { + $data['meeting_occurring'] = $this->generateOccurringString($eventReader); + } - $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM)); - $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM)); + return $data; + } - if ($dtstart instanceof Property\ICalendar\Date) { - // One day event - if ($diff->days === 1) { - return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); - } + /** + * generates a when string based on if a event has an recurrence or not + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenString(EventReader $er): string { + return match ($er->recurs()) { + true => $this->generateWhenStringRecurring($er), + false => $this->generateWhenStringSingular($er) + }; + } - // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05, - // the email should show 2020-01-01 to 2020-01-04. - $dtendDt->modify('-1 day'); + /** + * generates a when string for a non recurring event + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringSingular(EventReader $er): string { + // initialize + $startTime = null; + $endTime = null; + // calculate time difference from now to start of event + $occurring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate())); + // extract start date + $startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']); + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order: + // In a minute/hour/day/week/month/year on July 1, 2024 for the entire day + // In a minute/hour/day/week/month/year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto) + // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 for the entire day + // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto) + return match ([$occurring['scale'], $endTime !== null]) { + ['past', false] => $this->l10n->t( + 'In the past on %1$s for the entire day', + [$startDate] + ), + ['minute', false] => $this->l10n->n( + 'In a minute on %1$s for the entire day', + 'In %n minutes on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['hour', false] => $this->l10n->n( + 'In a hour on %1$s for the entire day', + 'In %n hours on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['day', false] => $this->l10n->n( + 'In a day on %1$s for the entire day', + 'In %n days on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['week', false] => $this->l10n->n( + 'In a week on %1$s for the entire day', + 'In %n weeks on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['month', false] => $this->l10n->n( + 'In a month on %1$s for the entire day', + 'In %n months on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['year', false] => $this->l10n->n( + 'In a year on %1$s for the entire day', + 'In %n years on %1$s for the entire day', + $occurring['interval'], + [$startDate] + ), + ['past', true] => $this->l10n->t( + 'In the past on %1$s between %2$s - %3$s', + [$startDate, $startTime, $endTime] + ), + ['minute', true] => $this->l10n->n( + 'In a minute on %1$s between %2$s - %3$s', + 'In %n minutes on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + ['hour', true] => $this->l10n->n( + 'In a hour on %1$s between %2$s - %3$s', + 'In %n hours on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + ['day', true] => $this->l10n->n( + 'In a day on %1$s between %2$s - %3$s', + 'In %n days on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + ['week', true] => $this->l10n->n( + 'In a week on %1$s between %2$s - %3$s', + 'In %n weeks on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + ['month', true] => $this->l10n->n( + 'In a month on %1$s between %2$s - %3$s', + 'In %n months on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + ['year', true] => $this->l10n->n( + 'In a year on %1$s between %2$s - %3$s', + 'In %n years on %1$s between %2$s - %3$s', + $occurring['interval'], + [$startDate, $startTime, $endTime] + ), + default => $this->l10n->t('Could not generate when statement') + }; + } - //event that spans over multiple days - $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); - $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']); + /** + * generates a when string based on recurrence precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurring(EventReader $er): string { + return match ($er->recurringPrecision()) { + 'daily' => $this->generateWhenStringRecurringDaily($er), + 'weekly' => $this->generateWhenStringRecurringWeekly($er), + 'monthly' => $this->generateWhenStringRecurringMonthly($er), + 'yearly' => $this->generateWhenStringRecurringYearly($er), + 'fixed' => $this->generateWhenStringRecurringFixed($er), + }; + } - return $localeStart . ' - ' . $localeEnd; + /** + * generates a when string for a daily precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurringDaily(EventReader $er): string { + + // initialize + $interval = (int)$er->recurringInterval(); + $startTime = null; + $conclusion = null; + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; } + // conclusion + if ($er->recurringConcludes()) { + $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']); + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order: + // Every Day for the entire day + // Every Day for the entire day until July 13, 2024 + // Every Day between 8:00 AM - 9:00 AM (America/Toronto) + // Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024 + // Every 3 Days for the entire day + // Every 3 Days for the entire day until July 13, 2024 + // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) + // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024 + return match ([($interval > 1), $startTime !== null, $conclusion !== null]) { + [false, false, false] => $this->l10n->t('Every Day for the entire day'), + [false, false, true] => $this->l10n->t('Every Day for the entire day until %1$s', [$conclusion]), + [false, true, false] => $this->l10n->t('Every Day between %1$s - %2$s', [$startTime, $endTime]), + [false, true, true] => $this->l10n->t('Every Day between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]), + [true, false, false] => $this->l10n->t('Every %1$d Days for the entire day', [$interval]), + [true, false, true] => $this->l10n->t('Every %1$d Days for the entire day until %2$s', [$interval, $conclusion]), + [true, true, false] => $this->l10n->t('Every %1$d Days between %2$s - %3$s', [$interval, $startTime, $endTime]), + [true, true, true] => $this->l10n->t('Every %1$d Days between %2$s - %3$s until %4$s', [$interval, $startTime, $endTime, $conclusion]), + default => $this->l10n->t('Could not generate event recurrence statement') + }; - /** @var Property\ICalendar\DateTime $dtstart */ - /** @var Property\ICalendar\DateTime $dtend */ - $isFloating = $dtstart->isFloating(); - $startTimezone = $endTimezone = null; - if (!$isFloating) { - $prop = $dtstart->offsetGet('TZID'); - if ($prop instanceof Parameter) { - $startTimezone = $prop->getValue(); - } + } - $prop = $dtend->offsetGet('TZID'); - if ($prop instanceof Parameter) { - $endTimezone = $prop->getValue(); - } + /** + * generates a when string for a weekly precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurringWeekly(EventReader $er): string { + + // initialize + $interval = (int)$er->recurringInterval(); + $startTime = null; + $conclusion = null; + // days of the week + $days = implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed())); + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; } + // conclusion + if ($er->recurringConcludes()) { + $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']); + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order: + // Every Week on Monday, Wednesday, Friday for the entire day + // Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024 + // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) + // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024 + // Every 2 Weeks on Monday, Wednesday, Friday for the entire day + // Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024 + // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) + // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024 + return match ([($interval > 1), $startTime !== null, $conclusion !== null]) { + [false, false, false] => $this->l10n->t('Every Week on %1$s for the entire day', [$days]), + [false, false, true] => $this->l10n->t('Every Week on %1$s for the entire day until %2$s', [$days, $conclusion]), + [false, true, false] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s', [$days, $startTime, $endTime]), + [false, true, true] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]), + [true, false, false] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day', [$interval, $days]), + [true, false, true] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day until %3$s', [$interval, $days, $conclusion]), + [true, true, false] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]), + [true, true, true] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]), + default => $this->l10n->t('Could not generate event recurrence statement') + }; - $localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' . - $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']); - - // always show full date with timezone if timezones are different - if ($startTimezone !== $endTimezone) { - $localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); + } - return $localeStart . ' (' . $startTimezone . ') - ' . - $localeEnd . ' (' . $endTimezone . ')'; + /** + * generates a when string for a monthly precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurringMonthly(EventReader $er): string { + + // initialize + $interval = (int)$er->recurringInterval(); + $startTime = null; + $conclusion = null; + // days of month + if ($er->recurringPattern() === 'R') { + $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' + . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed())); + } else { + $days = implode(', ', $er->recurringDaysOfMonth()); + } + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; } + // conclusion + if ($er->recurringConcludes()) { + $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']); + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order, output varies depending on if the event is absolute or releative: + // Absolute: Every Month on the 1, 8 for the entire day + // Relative: Every Month on the First Sunday, Saturday for the entire day + // Absolute: Every Month on the 1, 8 for the entire day until December 31, 2024 + // Relative: Every Month on the First Sunday, Saturday for the entire day until December 31, 2024 + // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) + // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) + // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024 + // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024 + // Absolute: Every 2 Months on the 1, 8 for the entire day + // Relative: Every 2 Months on the First Sunday, Saturday for the entire day + // Absolute: Every 2 Months on the 1, 8 for the entire day until December 31, 2024 + // Relative: Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024 + // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) + // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) + // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024 + // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024 + return match ([($interval > 1), $startTime !== null, $conclusion !== null]) { + [false, false, false] => $this->l10n->t('Every Month on the %1$s for the entire day', [$days]), + [false, false, true] => $this->l10n->t('Every Month on the %1$s for the entire day until %2$s', [$days, $conclusion]), + [false, true, false] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s', [$days, $startTime, $endTime]), + [false, true, true] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]), + [true, false, false] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day', [$interval, $days]), + [true, false, true] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day until %3$s', [$interval, $days, $conclusion]), + [true, true, false] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]), + [true, true, true] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]), + default => $this->l10n->t('Could not generate event recurrence statement') + }; + } - // show only end time if date is the same - if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) { - $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']); + /** + * generates a when string for a yearly precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurringYearly(EventReader $er): string { + + // initialize + $interval = (int)$er->recurringInterval(); + $startTime = null; + $conclusion = null; + // months of year + $months = implode(', ', array_map(function ($value) { return $this->localizeMonthName($value); }, $er->recurringMonthsOfYearNamed())); + // days of month + if ($er->recurringPattern() === 'R') { + $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' + . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed())); } else { - $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' . - $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); + $days = $er->startDateTime()->format('jS'); + } + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; } + // conclusion + if ($er->recurringConcludes()) { + $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']); + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order, output varies depending on if the event is absolute or releative: + // Absolute: Every Year in July on the 1st for the entire day + // Relative: Every Year in July on the First Sunday, Saturday for the entire day + // Absolute: Every Year in July on the 1st for the entire day until July 31, 2026 + // Relative: Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026 + // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) + // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) + // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026 + // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026 + // Absolute: Every 2 Years in July on the 1st for the entire day + // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day + // Absolute: Every 2 Years in July on the 1st for the entire day until July 31, 2026 + // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026 + // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) + // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) + // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026 + // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026 + return match ([($interval > 1), $startTime !== null, $conclusion !== null]) { + [false, false, false] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day', [$months, $days]), + [false, false, true] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day until %3$s', [$months, $days, $conclusion]), + [false, true, false] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s', [$months, $days, $startTime, $endTime]), + [false, true, true] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', [$months, $days, $startTime, $endTime, $conclusion]), + [true, false, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day', [$interval, $months, $days]), + [true, false, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [$interval, $months, $days, $conclusion]), + [true, true, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [$interval, $months, $days, $startTime, $endTime]), + [true, true, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [$interval, $months, $days, $startTime, $endTime, $conclusion]), + default => $this->l10n->t('Could not generate event recurrence statement') + }; + } + + /** + * generates a when string for a fixed precision/frequency + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateWhenStringRecurringFixed(EventReader $er): string { + // initialize + $startTime = null; + $conclusion = null; + // time of the day + if (!$er->entireDay()) { + $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']); + $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : ''; + $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')'; + } + // conclusion + $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']); + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order: + // On specific dates for the entire day until July 13, 2024 + // On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024 + return match ($startTime !== null) { + false => $this->l10n->t('On specific dates for the entire day until %1$s', [$conclusion]), + true => $this->l10n->t('On specific dates between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]), + }; + } + + /** + * generates a occurring next string for a recurring event + * + * @since 30.0.0 + * + * @param EventReader $er + * + * @return string + */ + public function generateOccurringString(EventReader $er): string { + + // initialize + $occurrence = null; + $occurrence2 = null; + $occurrence3 = null; + // reset to initial occurrence + $er->recurrenceRewind(); + // forward to current date + $er->recurrenceAdvanceTo($this->timeFactory->getDateTime()); + // calculate time difference from now to start of next event occurrence and minimize it + $occurrenceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate())); + // store next occurrence value + $occurrence = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']); + // forward one occurrence + $er->recurrenceAdvance(); + // evaluate if occurrence is valid + if ($er->recurrenceDate() !== null) { + // store following occurrence value + $occurrence2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']); + // forward one occurrence + $er->recurrenceAdvance(); + // evaluate if occurrence is valid + if ($er->recurrenceDate()) { + // store following occurrence value + $occurrence3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']); + } + } + // generate localized when string + // TRANSLATORS + // Indicates when a calendar event will happen, shown on invitation emails + // Output produced in order: + // In a minute/hour/day/week/month/year on July 1, 2024 + // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024 + // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024 and July 5, 2024 + // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 + // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024 + // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024 and July 5, 2024 + return match ([$occurrenceIn['scale'], $occurrence2 !== null, $occurrence3 !== null]) { + ['past', false, false] => $this->l10n->t( + 'In the past on %1$s', + [$occurrence] + ), + ['minute', false, false] => $this->l10n->n( + 'In a minute on %1$s', + 'In %n minutes on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['hour', false, false] => $this->l10n->n( + 'In a hour on %1$s', + 'In %n hours on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['day', false, false] => $this->l10n->n( + 'In a day on %1$s', + 'In %n days on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['week', false, false] => $this->l10n->n( + 'In a week on %1$s', + 'In %n weeks on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['month', false, false] => $this->l10n->n( + 'In a month on %1$s', + 'In %n months on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['year', false, false] => $this->l10n->n( + 'In a year on %1$s', + 'In %n years on %1$s', + $occurrenceIn['interval'], + [$occurrence] + ), + ['past', true, false] => $this->l10n->t( + 'In the past on %1$s then on %2$s', + [$occurrence, $occurrence2] + ), + ['minute', true, false] => $this->l10n->n( + 'In a minute on %1$s then on %2$s', + 'In %n minutes on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['hour', true, false] => $this->l10n->n( + 'In a hour on %1$s then on %2$s', + 'In %n hours on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['day', true, false] => $this->l10n->n( + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['week', true, false] => $this->l10n->n( + 'In a week on %1$s then on %2$s', + 'In %n weeks on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['month', true, false] => $this->l10n->n( + 'In a month on %1$s then on %2$s', + 'In %n months on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['year', true, false] => $this->l10n->n( + 'In a year on %1$s then on %2$s', + 'In %n years on %1$s then on %2$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2] + ), + ['past', true, true] => $this->l10n->t( + 'In the past on %1$s then on %2$s and %3$s', + [$occurrence, $occurrence2, $occurrence3] + ), + ['minute', true, true] => $this->l10n->n( + 'In a minute on %1$s then on %2$s and %3$s', + 'In %n minutes on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + ['hour', true, true] => $this->l10n->n( + 'In a hour on %1$s then on %2$s and %3$s', + 'In %n hours on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + ['day', true, true] => $this->l10n->n( + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + ['week', true, true] => $this->l10n->n( + 'In a week on %1$s then on %2$s and %3$s', + 'In %n weeks on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + ['month', true, true] => $this->l10n->n( + 'In a month on %1$s then on %2$s and %3$s', + 'In %n months on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + ['year', true, true] => $this->l10n->n( + 'In a year on %1$s then on %2$s and %3$s', + 'In %n years on %1$s then on %2$s and %3$s', + $occurrenceIn['interval'], + [$occurrence, $occurrence2, $occurrence3] + ), + default => $this->l10n->t('Could not generate next recurrence statement') + }; - return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')'; } /** @@ -278,12 +789,13 @@ class IMipService { * @return array */ public function buildCancelledBodyData(VEvent $vEvent): array { + // construct event reader + $eventReaderCurrent = new EventReader($vEvent); $defaultVal = ''; $strikethrough = "<span style='text-decoration: line-through'>%s</span>"; - $newMeetingWhen = $this->generateWhenString($vEvent); + $newMeetingWhen = $this->generateWhenString($eventReaderCurrent); $newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event'); - ; $newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal; $newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal; $newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal; @@ -337,7 +849,7 @@ class IMipService { return $dtEnd->getDateTime()->getTimeStamp(); } - if(isset($component->DURATION)) { + if (isset($component->DURATION)) { /** @var \DateTime $endDate */ $endDate = clone $dtStart->getDateTime(); // $component->DTEND->getDateTime() returns DateTimeImmutable @@ -345,7 +857,7 @@ class IMipService { return $endDate->getTimestamp(); } - if(!$dtStart->hasTime()) { + if (!$dtStart->hasTime()) { /** @var \DateTime $endDate */ // $component->DTSTART->getDateTime() returns DateTimeImmutable $endDate = clone $dtStart->getDateTime(); @@ -361,7 +873,7 @@ class IMipService { * @param Property|null $attendee */ public function setL10n(?Property $attendee = null) { - if($attendee === null) { + if ($attendee === null) { return; } @@ -377,7 +889,7 @@ class IMipService { * @return bool */ public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) { - if($attendee === null) { + if ($attendee === null) { return false; } @@ -485,10 +997,10 @@ class IMipService { htmlspecialchars($organizer->getNormalizedValue()), htmlspecialchars($organizerName ?: $organizerEmail)); $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail); - if(isset($organizer['PARTSTAT'])) { + if (isset($organizer['PARTSTAT'])) { /** @var Parameter $partstat */ $partstat = $organizer['PARTSTAT']; - if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { + if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { $organizerHTML .= ' ✔︎'; $organizerText .= ' ✔︎'; } @@ -539,7 +1051,7 @@ class IMipService { $data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'), $this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT); if ($data['meeting_when'] !== '') { - $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'), + $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('When:'), $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT); } if ($data['meeting_location'] !== '') { @@ -550,6 +1062,10 @@ class IMipService { $template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'), $this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT); } + if (isset($data['meeting_occurring'])) { + $template->addBodyListItem($data['meeting_occurring_html'] ?? $data['meeting_occurring'], $this->l10n->t('Occurring:'), + $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_occurring'], '', IMipPlugin::IMIP_INDENT); + } $this->addAttendees($template, $vevent); @@ -569,8 +1085,11 @@ class IMipService { $vevent = $iTipMessage->message->VEVENT; $attendees = $vevent->select('ATTENDEE'); foreach ($attendees as $attendee) { - /** @var Property $attendee */ - if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) { + if ($iTipMessage->method === 'REPLY' && strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) { + /** @var Property $attendee */ + return $attendee; + } elseif (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) { + /** @var Property $attendee */ return $attendee; } } @@ -589,9 +1108,9 @@ class IMipService { $attendee = $iTipMessage->recipient; $organizer = $iTipMessage->sender; $sequence = $iTipMessage->sequence; - $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? - $vevent->{'RECURRENCE-ID'}->serialize() : null; - $uid = $vevent->{'UID'}; + $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) + ? $vevent->{'RECURRENCE-ID'}->serialize() : null; + $uid = $vevent->{'UID'}?->getValue(); $query = $this->db->getQueryBuilder(); $query->insert('calendar_invitations') @@ -604,37 +1123,12 @@ class IMipService { 'expiration' => $query->createNamedParameter($lastOccurrence), 'uid' => $query->createNamedParameter($uid) ]) - ->execute(); + ->executeStatement(); return $token; } /** - * Create a valid VCalendar object out of the details of - * a VEvent and its associated iTip Message - * - * We do this to filter out all unchanged VEvents - * This is especially important in iTip Messages with recurrences - * and recurrence exceptions - * - * @param Message $iTipMessage - * @param VEvent $vEvent - * @return VCalendar - */ - public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar { - $vCalendar = new VCalendar(); - $vCalendar->add('METHOD', $iTipMessage->method); - foreach ($iTipMessage->message->getComponents() as $component) { - if ($component instanceof VEvent) { - continue; - } - $vCalendar->add(clone $component); - } - $vCalendar->add($vEvent); - return $vCalendar; - } - - /** * @param IEMailTemplate $template * @param $token */ @@ -678,14 +1172,123 @@ class IMipService { public function isRoomOrResource(Property $attendee): bool { $cuType = $attendee->offsetGet('CUTYPE'); - if(!$cuType instanceof Parameter) { + if (!$cuType instanceof Parameter) { return false; } $type = $cuType->getValue() ?? 'INDIVIDUAL'; - if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM', 'UNKNOWN'], true)) { + if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM'], true)) { // Don't send emails to things return true; } return false; } + + public function isCircle(Property $attendee): bool { + $cuType = $attendee->offsetGet('CUTYPE'); + if (!$cuType instanceof Parameter) { + return false; + } + + $uri = $attendee->getValue(); + if (!$uri) { + return false; + } + + $cuTypeValue = $cuType->getValue(); + return $cuTypeValue === 'GROUP' && str_starts_with($uri, 'mailto:circle+'); + } + + public function minimizeInterval(\DateInterval $dateInterval): array { + // evaluate if time interval is in the past + if ($dateInterval->invert == 1) { + return ['interval' => 1, 'scale' => 'past']; + } + // evaluate interval parts and return smallest time period + if ($dateInterval->y > 0) { + $interval = $dateInterval->y; + $scale = 'year'; + } elseif ($dateInterval->m > 0) { + $interval = $dateInterval->m; + $scale = 'month'; + } elseif ($dateInterval->d >= 7) { + $interval = (int)($dateInterval->d / 7); + $scale = 'week'; + } elseif ($dateInterval->d > 0) { + $interval = $dateInterval->d; + $scale = 'day'; + } elseif ($dateInterval->h > 0) { + $interval = $dateInterval->h; + $scale = 'hour'; + } else { + $interval = $dateInterval->i; + $scale = 'minute'; + } + + return ['interval' => $interval, 'scale' => $scale]; + } + + /** + * Localizes week day names to another language + * + * @param string $value + * + * @return string + */ + public function localizeDayName(string $value): string { + return match ($value) { + 'Monday' => $this->l10n->t('Monday'), + 'Tuesday' => $this->l10n->t('Tuesday'), + 'Wednesday' => $this->l10n->t('Wednesday'), + 'Thursday' => $this->l10n->t('Thursday'), + 'Friday' => $this->l10n->t('Friday'), + 'Saturday' => $this->l10n->t('Saturday'), + 'Sunday' => $this->l10n->t('Sunday'), + }; + } + + /** + * Localizes month names to another language + * + * @param string $value + * + * @return string + */ + public function localizeMonthName(string $value): string { + return match ($value) { + 'January' => $this->l10n->t('January'), + 'February' => $this->l10n->t('February'), + 'March' => $this->l10n->t('March'), + 'April' => $this->l10n->t('April'), + 'May' => $this->l10n->t('May'), + 'June' => $this->l10n->t('June'), + 'July' => $this->l10n->t('July'), + 'August' => $this->l10n->t('August'), + 'September' => $this->l10n->t('September'), + 'October' => $this->l10n->t('October'), + 'November' => $this->l10n->t('November'), + 'December' => $this->l10n->t('December'), + }; + } + + /** + * Localizes relative position names to another language + * + * @param string $value + * + * @return string + */ + public function localizeRelativePositionName(string $value): string { + return match ($value) { + 'First' => $this->l10n->t('First'), + 'Second' => $this->l10n->t('Second'), + 'Third' => $this->l10n->t('Third'), + 'Fourth' => $this->l10n->t('Fourth'), + 'Fifth' => $this->l10n->t('Fifth'), + 'Last' => $this->l10n->t('Last'), + 'Second Last' => $this->l10n->t('Second Last'), + 'Third Last' => $this->l10n->t('Third Last'), + 'Fourth Last' => $this->l10n->t('Fourth Last'), + 'Fifth Last' => $this->l10n->t('Fifth Last'), + }; + } } diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index a1297fd2cf1..a001df8b2a8 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -1,31 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Schedule; @@ -33,11 +10,15 @@ use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\CalendarObject; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\TipBroker; use OCP\IConfig; use Psr\Log\LoggerInterface; use Sabre\CalDAV\ICalendar; use Sabre\CalDAV\ICalendarObject; use Sabre\CalDAV\Schedule\ISchedulingObject; +use Sabre\DAV\Exception as DavException; use Sabre\DAV\INode; use Sabre\DAV\IProperties; use Sabre\DAV\PropFind; @@ -61,11 +42,6 @@ use function Sabre\Uri\split; class Plugin extends \Sabre\CalDAV\Schedule\Plugin { - /** - * @var IConfig - */ - private $config; - /** @var ITip\Message[] */ private $schedulingResponses = []; @@ -74,14 +50,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type'; public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL'; - private LoggerInterface $logger; /** * @param IConfig $config */ - public function __construct(IConfig $config, LoggerInterface $logger) { - $this->config = $config; - $this->logger = $logger; + public function __construct( + private IConfig $config, + private LoggerInterface $logger, + private DefaultCalendarValidator $defaultCalendarValidator, + ) { } /** @@ -105,6 +82,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { } /** + * Returns an instance of the iTip\Broker. + */ + protected function createITipBroker(): TipBroker { + return new TipBroker(); + } + + /** * Allow manual setting of the object change URL * to support public write * @@ -155,6 +139,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { $result = []; } + // iterate through items and html decode values + foreach ($result as $key => $value) { + $result[$key] = urldecode($value); + } + return $result; } @@ -173,7 +162,47 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { } try { - parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew); + + // Do not generate iTip and iMip messages if scheduling is disabled for this message + if ($request->getHeader('x-nc-scheduling') === 'false') { + return; + } + + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + /** @var Calendar $calendarNode */ + $calendarNode = $this->server->tree->getNodeForPath($calendarPath); + // extract addresses for owner + $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner()); + // determine if request is from a sharee + if ($calendarNode->isShared()) { + // extract addresses for sharee and add to address collection + $addresses = array_merge( + $addresses, + $this->getAddressesForPrincipal($calendarNode->getPrincipalURI()) + ); + } + // determine if we are updating a calendar event + if (!$isNew) { + // retrieve current calendar event node + /** @var CalendarObject $currentNode */ + $currentNode = $this->server->tree->getNodeForPath($request->getPath()); + // convert calendar event string data to VCalendar object + /** @var \Sabre\VObject\Component\VCalendar $currentObject */ + $currentObject = Reader::read($currentNode->get()); + } else { + $currentObject = null; + } + // process request + $this->processICalendarChange($currentObject, $vCal, $addresses, [], $modified); + + if ($currentObject) { + // Destroy circular references so PHP will GC the object. + $currentObject->destroy(); + } + } catch (SameOrganizerForAllComponentsException $e) { $this->handleSameOrganizerException($e, $vCal, $calendarPath); } @@ -232,7 +261,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); $calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri); if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) { - $this->logger->debug('Calendar user type is room or resource, not processing further'); + $this->logger->debug('Calendar user type is neither room nor resource, not processing further'); return; } @@ -343,8 +372,8 @@ EOF; return null; } - $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') || - str_starts_with($principalUrl, 'principals/calendar-rooms'); + $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') + || str_starts_with($principalUrl, 'principals/calendar-rooms'); if (str_starts_with($principalUrl, 'principals/users')) { [, $userId] = split($principalUrl); @@ -379,11 +408,20 @@ EOF; * - isn't a calendar subscription * - user can write to it (no virtual/3rd-party calendars) * - calendar isn't a share + * - calendar supports VEVENTs */ foreach ($calendarHome->getChildren() as $node) { - if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) { - $userCalendars[] = $node; + if (!($node instanceof Calendar)) { + continue; } + + try { + $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node); + } catch (DavException $e) { + continue; + } + + $userCalendars[] = $node; } if (count($userCalendars) > 0) { @@ -392,12 +430,20 @@ EOF; } else { // Otherwise if we have really nothing, create a new calendar if ($currentCalendarDeleted) { - // If the calendar exists but is deleted, we need to purge it first - // This may cause some issues in a non synchronous database setup + // If the calendar exists but is in the trash bin, we try to rename its uri + // so that we can create the new one and still restore the previous one + // otherwise we just purge the calendar by removing it before recreating it $calendar = $this->getCalendar($calendarHome, $uri); if ($calendar instanceof Calendar) { - $calendar->disableTrashbin(); - $calendar->delete(); + $backend = $calendarHome->getCalDAVBackend(); + if ($backend instanceof CalDavBackend) { + // If the CalDAV backend supports moving calendars + $this->moveCalendar($backend, $principalUrl, $uri, $uri . '-back-' . time()); + } else { + // Otherwise just purge the calendar + $calendar->disableTrashbin(); + $calendar->delete(); + } } } $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName); @@ -532,7 +578,9 @@ EOF; $calendarTimeZone = new DateTimeZone('UTC'); $homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref(); + /** @var Calendar $node */ foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) { + if (!$node instanceof ICalendar) { continue; } @@ -664,6 +712,10 @@ EOF; ]); } + private function moveCalendar(CalDavBackend $calDavBackend, string $principalUri, string $oldUri, string $newUri): void { + $calDavBackend->moveCalendar($oldUri, $principalUri, $principalUri, $newUri); + } + /** * Try to handle the given exception gracefully or throw it if necessary. * diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php index d08a5749ab2..27e39a76305 100644 --- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php +++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php @@ -1,33 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search; use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport; +use OCP\AppFramework\Http; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -81,8 +62,8 @@ class SearchPlugin extends ServerPlugin { $server->on('report', [$this, 'report']); - $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] = - CalendarSearchReport::class; + $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] + = CalendarSearchReport::class; } /** @@ -129,7 +110,7 @@ class SearchPlugin extends ServerPlugin { * This report is used by clients to request calendar objects based on * complex conditions. * - * @param Xml\Request\CalendarSearchReport $report + * @param CalendarSearchReport $report * @return void */ private function calendarSearch($report) { @@ -154,7 +135,7 @@ class SearchPlugin extends ServerPlugin { $prefer = $this->server->getHTTPPrefer(); - $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php index d5b7c834e36..21a4fff1caf 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php index 2c435ba3650..a98b325397b 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php index a6f41d09161..ef438aa0258 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php index c25450a0c94..0c31f32348a 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php index 990b0ebf730..251120e35cc 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php index 06fe39a463b..6d6bf958496 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Filter; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php index 98efe36ee43..6ece88fa87b 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Search\Xml\Request; diff --git a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php index 8ee4c9cb4d3..311157994e2 100644 --- a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php +++ b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php @@ -2,25 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Security; @@ -41,24 +25,16 @@ use function explode; class RateLimitingPlugin extends ServerPlugin { private Limiter $limiter; - private IUserManager $userManager; - private CalDavBackend $calDavBackend; - private IAppConfig $config; - private LoggerInterface $logger; - private ?string $userId; - public function __construct(Limiter $limiter, - IUserManager $userManager, - CalDavBackend $calDavBackend, - LoggerInterface $logger, - IAppConfig $config, - ?string $userId) { + public function __construct( + Limiter $limiter, + private IUserManager $userManager, + private CalDavBackend $calDavBackend, + private LoggerInterface $logger, + private IAppConfig $config, + private ?string $userId, + ) { $this->limiter = $limiter; - $this->userManager = $userManager; - $this->calDavBackend = $calDavBackend; - $this->config = $config; - $this->logger = $logger; - $this->userId = $userId; } public function initialize(DAV\Server $server): void { diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php index 7a87f0353e7..fc5d65b5994 100644 --- a/apps/dav/lib/CalDAV/Sharing/Backend.php +++ b/apps/dav/lib/CalDAV/Sharing/Backend.php @@ -2,22 +2,8 @@ declare(strict_types=1); /** - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Sharing; @@ -31,7 +17,8 @@ use Psr\Log\LoggerInterface; class Backend extends SharingBackend { - public function __construct(private IUserManager $userManager, + public function __construct( + private IUserManager $userManager, private IGroupManager $groupManager, private Principal $principalBackend, private ICacheFactory $cacheFactory, diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php index cdf8c892ab5..4f0554f09bd 100644 --- a/apps/dav/lib/CalDAV/Sharing/Service.php +++ b/apps/dav/lib/CalDAV/Sharing/Service.php @@ -2,22 +2,8 @@ declare(strict_types=1); /** - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Sharing; @@ -27,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService; class Service extends SharingService { protected string $resourceType = 'calendar'; - public function __construct(protected SharingMapper $mapper) { + public function __construct( + protected SharingMapper $mapper, + ) { parent::__construct($mapper); } } diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php index 29129a3b073..9ee0e9bf356 100644 --- a/apps/dav/lib/CalDAV/Status/StatusService.php +++ b/apps/dav/lib/CalDAV/Status/StatusService.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Status; @@ -32,6 +15,7 @@ use OCA\UserStatus\Service\StatusService as UserStatusService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\IManager; +use OCP\DB\Exception; use OCP\ICache; use OCP\ICacheFactory; use OCP\IUser as User; @@ -43,36 +27,49 @@ use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; class StatusService { private ICache $cache; - public function __construct(private ITimeFactory $timeFactory, + public function __construct( + private ITimeFactory $timeFactory, private IManager $calendarManager, private IUserManager $userManager, private UserStatusService $userStatusService, private IAvailabilityCoordinator $availabilityCoordinator, private ICacheFactory $cacheFactory, - private LoggerInterface $logger) { + private LoggerInterface $logger, + ) { $this->cache = $cacheFactory->createLocal('CalendarStatusService'); } public function processCalendarStatus(string $userId): void { $user = $this->userManager->get($userId); - if($user === null) { + if ($user === null) { return; } $availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); - if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) { + if ($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) { $this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]); return; } $calendarEvents = $this->cache->get($userId); - if($calendarEvents === null) { + if ($calendarEvents === null) { $calendarEvents = $this->getCalendarEvents($user); $this->cache->set($userId, $calendarEvents, 300); } - if(empty($calendarEvents)) { - $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY); + if (empty($calendarEvents)) { + try { + $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY); + } catch (Exception $e) { + if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + // A different process might have written another status + // update to the DB while we're processing our stuff. + // We cannot safely restore the status as we don't know which one is valid at this point + // So let's silently log this one and exit + $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]); + return; + } + } $this->logger->debug('No calendar events found for status check', ['user' => $userId]); return; } @@ -86,9 +83,9 @@ class StatusService { $currentStatus = null; } - if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL - || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND - || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) { + if (($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL) + || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND) + || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE)) { // We don't overwrite the call status, DND status or Invisible status $this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]); return; @@ -106,7 +103,7 @@ class StatusService { if (isset($component['DTSTART']) && $userStatusTimestamp !== null) { /** @var DateTimeImmutable $dateTime */ $dateTime = $component['DTSTART'][0]; - if($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) { + if ($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) { return false; } } @@ -117,14 +114,25 @@ class StatusService { return true; }); - if(empty($applicableEvents)) { - $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY); + if (empty($applicableEvents)) { + try { + $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY); + } catch (Exception $e) { + if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + // A different process might have written another status + // update to the DB while we're processing our stuff. + // We cannot safely restore the status as we don't know which one is valid at this point + // So let's silently log this one and exit + $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]); + return; + } + } $this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]); return; } // Only update the status if it's neccesary otherwise we mess up the timestamp - if($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) { + if ($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) { // One event that fulfills all status conditions is enough // 1. Not an OOO event // 2. Current user status (that is not a calendar status) was not set after the start of this event @@ -133,7 +141,7 @@ class StatusService { $this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]); $this->userStatusService->setUserStatus( $userId, - IUserStatus::AWAY, + IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, true ); @@ -142,7 +150,7 @@ class StatusService { private function getCalendarEvents(User $user): array { $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID()); - if(empty($calendars)) { + if (empty($calendars)) { return []; } @@ -166,7 +174,7 @@ class StatusService { $dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes')); // Only query the calendars when there's any to search - if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) { + if ($query instanceof CalendarQuery && !empty($query->getCalendarUris())) { // Query the next hour $query->setTimerangeStart($dtStart); $query->setTimerangeEnd($dtEnd); diff --git a/apps/dav/lib/CalDAV/TimeZoneFactory.php b/apps/dav/lib/CalDAV/TimeZoneFactory.php new file mode 100644 index 00000000000..36a2c97be82 --- /dev/null +++ b/apps/dav/lib/CalDAV/TimeZoneFactory.php @@ -0,0 +1,213 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use DateTimeZone; + +/** + * Class to generate DateTimeZone object with automated Microsoft and IANA handling + * + * @since 31.0.0 + */ +class TimeZoneFactory { + + /** + * conversion table of Microsoft time zones to IANA time zones + * + * @var array<string,string> MS2IANA + */ + private const MS2IANA = [ + 'AUS Central Standard Time' => 'Australia/Darwin', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Canada Central Standard Time' => 'America/Regina', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Coordinated Universal Time' => 'UTC', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/Toronto', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenland (Danmarkshavn)' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Kolkata', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Kamchatka Standard Time' => 'Asia/Kamchatka', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Malaysia Standard Time' => 'Asia/Kuala_Lumpur', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Kathmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russian Standard Time' => 'Europe/Moscow', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 3' => 'Europe/Samara', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'South Sudan Standard Time' => 'Africa/Juba', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+13' => 'Etc/GMT-13', + 'UTC+12' => 'Etc/GMT-12', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'West Samoa Standard Time' => 'Pacific/Apia', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', + 'Yekaterinburg Standard Time' => 'Asia/Yekaterinburg', + ]; + + /** + * Determines if given time zone name is a Microsoft time zone + * + * @since 31.0.0 + * + * @param string $name time zone name + * + * @return bool + */ + public static function isMS(string $name): bool { + return isset(self::MS2IANA[$name]); + } + + /** + * Converts Microsoft time zone name to IANA time zone name + * + * @since 31.0.0 + * + * @param string $name microsoft time zone + * + * @return string|null valid IANA time zone name on success, or null on failure + */ + public static function toIANA(string $name): ?string { + return isset(self::MS2IANA[$name]) ? self::MS2IANA[$name] : null; + } + + /** + * Generates DateTimeZone object for given time zone name + * + * @since 31.0.0 + * + * @param string $name time zone name + * + * @return DateTimeZone|null + */ + public function fromName(string $name): ?DateTimeZone { + // if zone name is MS convert to IANA, otherwise just assume the zone is IANA + $zone = @timezone_open(self::toIANA($name) ?? $name); + return ($zone instanceof DateTimeZone) ? $zone : null; + } +} diff --git a/apps/dav/lib/CalDAV/TimezoneService.php b/apps/dav/lib/CalDAV/TimezoneService.php index 1b8855a7215..a7709bde0f9 100644 --- a/apps/dav/lib/CalDAV/TimezoneService.php +++ b/apps/dav/lib/CalDAV/TimezoneService.php @@ -2,25 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV; @@ -36,9 +20,11 @@ use function array_reduce; class TimezoneService { - public function __construct(private IConfig $config, + public function __construct( + private IConfig $config, private PropertyMapper $propertyMapper, - private IManager $calendarManager) { + private IManager $calendarManager, + ) { } public function getUserTimezone(string $userId): ?string { diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php new file mode 100644 index 00000000000..16e68fde1f0 --- /dev/null +++ b/apps/dav/lib/CalDAV/TipBroker.php @@ -0,0 +1,187 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\ITip\Broker; +use Sabre\VObject\ITip\Message; + +class TipBroker extends Broker { + + public $significantChangeProperties = [ + 'DTSTART', + 'DTEND', + 'DURATION', + 'DUE', + 'RRULE', + 'RDATE', + 'EXDATE', + 'STATUS', + 'SUMMARY', + 'DESCRIPTION', + 'LOCATION', + + ]; + + /** + * This method is used in cases where an event got updated, and we + * potentially need to send emails to attendees to let them know of updates + * in the events. + * + * We will detect which attendees got added, which got removed and create + * specific messages for these situations. + * + * @return array + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) { + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + + foreach ($attendees as $attendee) { + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. + if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') { + + $message->method = $icalMsg->METHOD = 'CANCEL'; + $message->significantChange = true; + // clone base event + $event = clone $eventInfo['instances']['master']; + // alter some properties + unset($event->ATTENDEE); + $event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]); + $event->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $event->SEQUENCE = $message->sequence; + $icalMsg->add($event); + + } else { + // The attendee gets the updated event body + $message->method = $icalMsg->METHOD = 'REQUEST'; + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $oldAttendeeInstances = array_keys($attendee['oldInstances']); + $newAttendeeInstances = array_keys($attendee['newInstances']); + + $message->significantChange + = $attendee['forceSend'] === 'REQUEST' + || count($oldAttendeeInstances) !== count($newAttendeeInstances) + || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 + || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ($instanceId === 'master') { + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + } + } + + $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $icalMsg->add($currentEvent); + } + } + + $message->message = $icalMsg; + $messages[] = $message; + } + + return $messages; + } + +} diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php index b1a3fea21b7..d8c429f2056 100644 --- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Trashbin; @@ -35,26 +18,13 @@ use Sabre\DAVACL\IACL; class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable { use ACLTrait; - /** @var string */ - private $name; - - /** @var mixed[] */ - private $objectData; - - /** @var string */ - private $principalUri; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(string $name, - array $objectData, - string $principalUri, - CalDavBackend $calDavBackend) { - $this->name = $name; - $this->objectData = $objectData; - $this->calDavBackend = $calDavBackend; - $this->principalUri = $principalUri; + public function __construct( + private string $name, + /** @var mixed[] */ + private array $objectData, + private string $principalUri, + private CalDavBackend $calDavBackend, + ) { } public function delete() { @@ -89,7 +59,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable { public function getContentType() { $mime = 'text/calendar; charset=utf-8'; if (isset($this->objectData['component']) && $this->objectData['component']) { - $mime .= '; component='.$this->objectData['component']; + $mime .= '; component=' . $this->objectData['component']; } return $mime; @@ -100,7 +70,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable { } public function getSize() { - return (int) $this->objectData['size']; + return (int)$this->objectData['size']; } public function restore(): void { @@ -108,7 +78,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable { } public function getDeletedAt(): ?int { - return $this->objectData['deleted_at'] ? (int) $this->objectData['deleted_at'] : null; + return $this->objectData['deleted_at'] ? (int)$this->objectData['deleted_at'] : null; } public function getCalendarUri(): string { diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php index 5362b45ee7b..f75e19689f1 100644 --- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Trashbin; @@ -42,16 +25,11 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL public const NAME = 'objects'; - /** @var CalDavBackend */ - protected $caldavBackend; - - /** @var mixed[] */ - private $principalInfo; - - public function __construct(CalDavBackend $caldavBackend, - array $principalInfo) { - $this->caldavBackend = $caldavBackend; - $this->principalInfo = $principalInfo; + public function __construct( + protected CalDavBackend $caldavBackend, + /** @var mixed[] */ + private array $principalInfo, + ) { } /** @@ -68,7 +46,7 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL $data = $this->caldavBackend->getCalendarObjectById( $this->principalInfo['uri'], - (int) $matches[1], + (int)$matches[1], ); // If the object hasn't been deleted yet then we don't want to find it here diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php index 8a661732f19..6f58b1f3110 100644 --- a/apps/dav/lib/CalDAV/Trashbin/Plugin.php +++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Trashbin; @@ -49,16 +32,14 @@ class Plugin extends ServerPlugin { /** @var bool */ private $disableTrashbin; - /** @var RetentionService */ - private $retentionService; - /** @var Server */ private $server; - public function __construct(IRequest $request, - RetentionService $retentionService) { + public function __construct( + IRequest $request, + private RetentionService $retentionService, + ) { $this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1'; - $this->retentionService = $retentionService; } public function initialize(Server $server): void { diff --git a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php index 31331957c49..6641148de2b 100644 --- a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php +++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Trashbin; diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php index e9bf6da19e8..1c76bd2295d 100644 --- a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php +++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\Trashbin; @@ -43,16 +26,10 @@ class TrashbinHome implements IACL, ICollection, IProperties { public const NAME = 'trashbin'; - /** @var CalDavBackend */ - private $caldavBackend; - - /** @var array */ - private $principalInfo; - - public function __construct(CalDavBackend $caldavBackend, - array $principalInfo) { - $this->caldavBackend = $caldavBackend; - $this->principalInfo = $principalInfo; + public function __construct( + private CalDavBackend $caldavBackend, + private array $principalInfo, + ) { } public function getOwner(): string { diff --git a/apps/dav/lib/CalDAV/UpcomingEvent.php b/apps/dav/lib/CalDAV/UpcomingEvent.php new file mode 100644 index 00000000000..e8b604f460a --- /dev/null +++ b/apps/dav/lib/CalDAV/UpcomingEvent.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use JsonSerializable; +use OCA\DAV\ResponseDefinitions; + +class UpcomingEvent implements JsonSerializable { + public function __construct( + private string $uri, + private ?int $recurrenceId, + private string $calendarUri, + private ?int $start, + private ?string $summary, + private ?string $location, + private ?string $calendarAppUrl, + ) { + } + + public function getUri(): string { + return $this->uri; + } + + public function getRecurrenceId(): ?int { + return $this->recurrenceId; + } + + public function getCalendarUri(): string { + return $this->calendarUri; + } + + public function getStart(): ?int { + return $this->start; + } + + public function getSummary(): ?string { + return $this->summary; + } + + public function getLocation(): ?string { + return $this->location; + } + + public function getCalendarAppUrl(): ?string { + return $this->calendarAppUrl; + } + + /** + * @see ResponseDefinitions + */ + public function jsonSerialize(): array { + return [ + 'uri' => $this->uri, + 'recurrenceId' => $this->recurrenceId, + 'calendarUri' => $this->calendarUri, + 'start' => $this->start, + 'summary' => $this->summary, + 'location' => $this->location, + 'calendarAppUrl' => $this->calendarAppUrl, + ]; + } +} diff --git a/apps/dav/lib/CalDAV/UpcomingEventsService.php b/apps/dav/lib/CalDAV/UpcomingEventsService.php new file mode 100644 index 00000000000..1a8aed5bd71 --- /dev/null +++ b/apps/dav/lib/CalDAV/UpcomingEventsService.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CalDAV; + +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use function array_map; + +class UpcomingEventsService { + public function __construct( + private IManager $calendarManager, + private ITimeFactory $timeFactory, + private IUserManager $userManager, + private IAppManager $appManager, + private IURLGenerator $urlGenerator, + ) { + } + + /** + * @return UpcomingEvent[] + */ + public function getEvents(string $userId, ?string $location = null): array { + $searchQuery = $this->calendarManager->newQuery('principals/users/' . $userId); + if ($location !== null) { + $searchQuery->addSearchProperty('LOCATION'); + $searchQuery->setSearchPattern($location); + } + $searchQuery->addType('VEVENT'); + $searchQuery->setLimit(3); + $now = $this->timeFactory->now(); + $searchQuery->setTimerangeStart($now->modify('-1 minute')); + $searchQuery->setTimerangeEnd($now->modify('+1 month')); + + $events = $this->calendarManager->searchForPrincipal($searchQuery); + $calendarAppEnabled = $this->appManager->isEnabledForUser( + 'calendar', + $this->userManager->get($userId), + ); + + return array_filter(array_map(function (array $event) use ($userId, $calendarAppEnabled) { + $calendarAppUrl = null; + + if ($calendarAppEnabled) { + $arguments = [ + 'objectId' => base64_encode($this->urlGenerator->getWebroot() . '/remote.php/dav/calendars/' . $userId . '/' . $event['calendar-uri'] . '/' . $event['uri']), + ]; + + if (isset($event['RECURRENCE-ID'])) { + $arguments['recurrenceId'] = $event['RECURRENCE-ID'][0]; + } + /** + * TODO: create a named, deep route in calendar (it's a code smell to just assume this route exists, find an abstraction) + * When changing, also adjust for: + * - spreed/lib/Service/CalendarIntegrationService.php#getDashboardEvents + * - spreed/lib/Service/CalendarIntegrationService.php#getMutualEvents + */ + $calendarAppUrl = $this->urlGenerator->linkToRouteAbsolute('calendar.view.indexdirect.edit', $arguments); + } + + if (isset($event['objects'][0]['STATUS']) && $event['objects'][0]['STATUS'][0] === 'CANCELLED') { + return false; + } + + return new UpcomingEvent( + $event['uri'], + ($event['objects'][0]['RECURRENCE-ID'][0] ?? null)?->getTimeStamp(), + $event['calendar-uri'], + $event['objects'][0]['DTSTART'][0]?->getTimestamp(), + $event['objects'][0]['SUMMARY'][0] ?? null, + $event['objects'][0]['LOCATION'][0] ?? null, + $calendarAppUrl, + ); + }, $events)); + } + +} diff --git a/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php new file mode 100644 index 00000000000..b647e63e67b --- /dev/null +++ b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CalDAV\Validation; + +use OCA\DAV\AppInfo\Application; +use OCP\IAppConfig; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class CalDavValidatePlugin extends ServerPlugin { + + public function __construct( + private IAppConfig $config, + ) { + } + + public function initialize(Server $server): void { + $server->on('beforeMethod:PUT', [$this, 'beforePut']); + } + + public function beforePut(RequestInterface $request, ResponseInterface $response): bool { + // evaluate if card size exceeds defined limit + $eventSizeLimit = $this->config->getValueInt(Application::APP_ID, 'event_size_limit', 10485760); + if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $eventSizeLimit) { + throw new Forbidden("VEvent or VTodo object exceeds $eventSizeLimit bytes"); + } + // all tests passed return true + return true; + } + +} diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Connection.php b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php new file mode 100644 index 00000000000..3d12c92c49a --- /dev/null +++ b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php @@ -0,0 +1,143 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CalDAV\WebcalCaching; + +use Exception; +use GuzzleHttp\RequestOptions; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\LocalServerException; +use OCP\IAppConfig; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Reader; + +class Connection { + public function __construct( + private IClientService $clientService, + private IAppConfig $config, + private LoggerInterface $logger, + ) { + } + + /** + * gets webcal feed from remote server + */ + public function queryWebcalFeed(array $subscription): ?string { + $subscriptionId = $subscription['id']; + $url = $this->cleanURL($subscription['source']); + if ($url === null) { + return null; + } + + $allowLocalAccess = $this->config->getValueString('dav', 'webcalAllowLocalAccess', 'no'); + + $params = [ + 'nextcloud' => [ + 'allow_local_address' => $allowLocalAccess === 'yes', + ], + RequestOptions::HEADERS => [ + 'User-Agent' => 'Nextcloud Webcal Service', + 'Accept' => 'text/calendar, application/calendar+json, application/calendar+xml', + ], + ]; + + $user = parse_url($subscription['source'], PHP_URL_USER); + $pass = parse_url($subscription['source'], PHP_URL_PASS); + if ($user !== null && $pass !== null) { + $params[RequestOptions::AUTH] = [$user, $pass]; + } + + try { + $client = $this->clientService->newClient(); + $response = $client->get($url, $params); + } catch (LocalServerException $ex) { + $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [ + 'exception' => $ex, + ]); + return null; + } catch (Exception $ex) { + $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [ + 'exception' => $ex, + ]); + return null; + } + + $body = $response->getBody(); + + $contentType = $response->getHeader('Content-Type'); + $contentType = explode(';', $contentType, 2)[0]; + switch ($contentType) { + case 'application/calendar+json': + try { + $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING); + } catch (Exception $ex) { + // In case of a parsing error return null + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); + return null; + } + return $jCalendar->serialize(); + + case 'application/calendar+xml': + try { + $xCalendar = Reader::readXML($body); + } catch (Exception $ex) { + // In case of a parsing error return null + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); + return null; + } + return $xCalendar->serialize(); + + case 'text/calendar': + default: + try { + $vCalendar = Reader::read($body); + } catch (Exception $ex) { + // In case of a parsing error return null + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); + return null; + } + return $vCalendar->serialize(); + } + } + + /** + * This method will strip authentication information and replace the + * 'webcal' or 'webcals' protocol scheme + * + * @param string $url + * @return string|null + */ + private function cleanURL(string $url): ?string { + $parsed = parse_url($url); + if ($parsed === false) { + return null; + } + + if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') { + $scheme = 'http'; + } else { + $scheme = 'https'; + } + + $host = $parsed['host'] ?? ''; + $port = isset($parsed['port']) ? ':' . $parsed['port'] : ''; + $path = $parsed['path'] ?? ''; + $query = isset($parsed['query']) ? '?' . $parsed['query'] : ''; + $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; + + $cleanURL = "$scheme://$host$port$path$query$fragment"; + // parse_url is giving some weird results if no url and no :// is given, + // so let's test the url again + $parsedClean = parse_url($cleanURL); + if ($parsedClean === false || !isset($parsedClean['host'])) { + return null; + } + + return $cleanURL; + } +} diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php index 8d4714d313a..e07be39c7b4 100644 --- a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php +++ b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php @@ -3,30 +3,12 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\WebcalCaching; -use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\CalendarRoot; use OCP\IRequest; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Server; @@ -41,10 +23,14 @@ class Plugin extends ServerPlugin { * that do not support subscriptions on their own * * /^MSFT-WIN-3/ - Windows 10 Calendar + * /Evolution/ - Gnome Calendar/Evolution + * /KIO/ - KDE PIM/Akonadi * @var string[] */ public const ENABLE_FOR_CLIENTS = [ - "/^MSFT-WIN-3/" + '/^MSFT-WIN-3/', + '/Evolution/', + '/KIO/' ]; /** @@ -71,6 +57,11 @@ class Plugin extends ServerPlugin { if ($magicHeader === 'On') { $this->enabled = true; } + + $isExportRequest = $request->getMethod() === 'GET' && array_key_exists('export', $request->getParams()); + if ($isExportRequest) { + $this->enabled = true; + } } /** @@ -85,7 +76,7 @@ class Plugin extends ServerPlugin { */ public function initialize(Server $server) { $this->server = $server; - $server->on('beforeMethod:*', [$this, 'beforeMethod']); + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 15); } /** @@ -107,16 +98,11 @@ class Plugin extends ServerPlugin { return; } - // $calendarHomePath will look like: calendars/username - $calendarHomePath = $pathParts[0] . '/' . $pathParts[1]; try { - $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath); - if (!($calendarHome instanceof CalendarHome)) { - //how did we end up here? - return; + $calendarRoot = $this->server->tree->getNodeForPath($pathParts[0]); + if ($calendarRoot instanceof CalendarRoot) { + $calendarRoot->enableReturnCachedSubscriptions($pathParts[1]); } - - $calendarHome->enableCachedSubscriptionsForThisRequest(); } catch (NotFound $ex) { return; } diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php index 4035f345876..a0981e6dec1 100644 --- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php +++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php @@ -3,46 +3,17 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr> - * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author eleith <online+github@eleith.com> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CalDAV\WebcalCaching; -use Exception; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; use OCA\DAV\CalDAV\CalDavBackend; -use OCP\Http\Client\IClientService; -use OCP\Http\Client\LocalServerException; -use OCP\IConfig; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use OCP\AppFramework\Utility\ITimeFactory; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\PropPatch; -use Sabre\DAV\Xml\Property\Href; use Sabre\VObject\Component; use Sabre\VObject\DateTimeParser; use Sabre\VObject\InvalidDataException; @@ -55,25 +26,17 @@ use function count; class RefreshWebcalService { - private CalDavBackend $calDavBackend; - - private IClientService $clientService; - - private IConfig $config; - - /** @var LoggerInterface */ - private LoggerInterface $logger; - public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate'; public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms'; public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments'; public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos'; - public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, LoggerInterface $logger) { - $this->calDavBackend = $calDavBackend; - $this->clientService = $clientService; - $this->config = $config; - $this->logger = $logger; + public function __construct( + private CalDavBackend $calDavBackend, + private LoggerInterface $logger, + private Connection $connection, + private ITimeFactory $time, + ) { } public function refreshSubscription(string $principalUri, string $uri) { @@ -83,11 +46,25 @@ class RefreshWebcalService { return; } - $webcalData = $this->queryWebcalFeed($subscription, $mutations); + // Check the refresh rate if there is any + if (!empty($subscription['{http://apple.com/ns/ical/}refreshrate'])) { + // add the refresh interval to the lastmodified timestamp + $refreshInterval = new \DateInterval($subscription['{http://apple.com/ns/ical/}refreshrate']); + $updateTime = $this->time->getDateTime(); + $updateTime->setTimestamp($subscription['lastmodified'])->add($refreshInterval); + if ($updateTime->getTimestamp() > $this->time->getTime()) { + return; + } + } + + + $webcalData = $this->connection->queryWebcalFeed($subscription); if (!$webcalData) { return; } + $localData = $this->calDavBackend->getLimitedCalendarObjects((int)$subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + $stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1; $stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1; $stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1; @@ -95,14 +72,10 @@ class RefreshWebcalService { try { $splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING); - // we wait with deleting all outdated events till we parsed the new ones - // in case the new calendar is broken and `new ICalendar` throws a ParseException - // the user will still see the old data - $this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']); - while ($vObject = $splitter->getNext()) { /** @var Component $vObject */ $compName = null; + $uid = null; foreach ($vObject->getComponents() as $component) { if ($component->name === 'VTIMEZONE') { @@ -117,21 +90,68 @@ class RefreshWebcalService { if ($stripAttachments) { unset($component->{'ATTACH'}); } + + $uid = $component->{ 'UID' }->getValue(); } if ($stripTodos && $compName === 'VTODO') { continue; } - $objectUri = $this->getRandomCalendarObjectUri(); - $calendarData = $vObject->serialize(); + if (!isset($uid)) { + continue; + } + try { - $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); - } catch (NoInstancesException | BadRequest $ex) { - $this->logger->error('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]); + $denormalized = $this->calDavBackend->getDenormalizedData($vObject->serialize()); + } catch (InvalidDataException|Forbidden $ex) { + $this->logger->warning('Unable to denormalize calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]); + continue; + } + + // Find all identical sets and remove them from the update + if (isset($localData[$uid]) && $denormalized['etag'] === $localData[$uid]['etag']) { + unset($localData[$uid]); + continue; + } + + $vObjectCopy = clone $vObject; + $identical = isset($localData[$uid]) && $this->compareWithoutDtstamp($vObjectCopy, $localData[$uid]); + if ($identical) { + unset($localData[$uid]); + continue; + } + + // Find all modified sets and update them + if (isset($localData[$uid]) && $denormalized['etag'] !== $localData[$uid]['etag']) { + $this->calDavBackend->updateCalendarObject($subscription['id'], $localData[$uid]['uri'], $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + unset($localData[$uid]); + continue; + } + + // Only entirely new events get created here + try { + $objectUri = $this->getRandomCalendarObjectUri(); + $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + } catch (NoInstancesException|BadRequest $ex) { + $this->logger->warning('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]); } } + $ids = array_map(static function ($dataSet): int { + return (int)$dataSet['id']; + }, $localData); + $uris = array_map(static function ($dataSet): string { + return $dataSet['uri']; + }, $localData); + + if (!empty($ids) && !empty($uris)) { + // Clean up on aisle 5 + // The only events left over in the $localData array should be those that don't exist upstream + // All deleted VObjects from upstream are removed + $this->calDavBackend->purgeCachedEventsForSubscription($subscription['id'], $ids, $uris); + } + $newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData); if ($newRefreshRate) { $mutations[self::REFRESH_RATE] = $newRefreshRate; @@ -139,7 +159,7 @@ class RefreshWebcalService { $this->updateSubscription($subscription, $mutations); } catch (ParseException $ex) { - $this->logger->error("Subscription {subscriptionId} could not be refreshed due to a parsing error", ['exception' => $ex, 'subscriptionId' => $subscription['id']]); + $this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]); } } @@ -161,111 +181,6 @@ class RefreshWebcalService { return $subscriptions[0]; } - /** - * gets webcal feed from remote server - */ - private function queryWebcalFeed(array $subscription, array &$mutations): ?string { - $client = $this->clientService->newClient(); - - $didBreak301Chain = false; - $latestLocation = null; - - $handlerStack = HandlerStack::create(); - $handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) { - return $request - ->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml') - ->withHeader('User-Agent', 'Nextcloud Webcal Service'); - })); - $handlerStack->push(Middleware::mapResponse(function (ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) { - if (!$didBreak301Chain) { - if ($response->getStatusCode() !== 301) { - $didBreak301Chain = true; - } else { - $latestLocation = $response->getHeader('Location'); - } - } - return $response; - })); - - $allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no'); - $subscriptionId = $subscription['id']; - $url = $this->cleanURL($subscription['source']); - if ($url === null) { - return null; - } - - try { - $params = [ - 'allow_redirects' => [ - 'redirects' => 10 - ], - 'handler' => $handlerStack, - 'nextcloud' => [ - 'allow_local_address' => $allowLocalAccess === 'yes', - ] - ]; - - $user = parse_url($subscription['source'], PHP_URL_USER); - $pass = parse_url($subscription['source'], PHP_URL_PASS); - if ($user !== null && $pass !== null) { - $params['auth'] = [$user, $pass]; - } - - $response = $client->get($url, $params); - $body = $response->getBody(); - - if ($latestLocation) { - $mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation); - } - - $contentType = $response->getHeader('Content-Type'); - $contentType = explode(';', $contentType, 2)[0]; - switch ($contentType) { - case 'application/calendar+json': - try { - $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING); - } catch (Exception $ex) { - // In case of a parsing error return null - $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); - return null; - } - return $jCalendar->serialize(); - - case 'application/calendar+xml': - try { - $xCalendar = Reader::readXML($body); - } catch (Exception $ex) { - // In case of a parsing error return null - $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); - return null; - } - return $xCalendar->serialize(); - - case 'text/calendar': - default: - try { - $vCalendar = Reader::read($body); - } catch (Exception $ex) { - // In case of a parsing error return null - $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); - return null; - } - return $vCalendar->serialize(); - } - } catch (LocalServerException $ex) { - $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [ - 'exception' => $ex, - ]); - - return null; - } catch (Exception $ex) { - $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [ - 'exception' => $ex, - ]); - - return null; - } - } /** * check if: @@ -326,47 +241,24 @@ class RefreshWebcalService { } /** - * This method will strip authentication information and replace the - * 'webcal' or 'webcals' protocol scheme + * Returns a random uri for a calendar-object * - * @param string $url - * @return string|null + * @return string */ - private function cleanURL(string $url): ?string { - $parsed = parse_url($url); - if ($parsed === false) { - return null; - } + public function getRandomCalendarObjectUri():string { + return UUIDUtil::getUUID() . '.ics'; + } - if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') { - $scheme = 'http'; - } else { - $scheme = 'https'; + private function compareWithoutDtstamp(Component $vObject, array $calendarObject): bool { + foreach ($vObject->getComponents() as $component) { + unset($component->{'DTSTAMP'}); } - $host = $parsed['host'] ?? ''; - $port = isset($parsed['port']) ? ':' . $parsed['port'] : ''; - $path = $parsed['path'] ?? ''; - $query = isset($parsed['query']) ? '?' . $parsed['query'] : ''; - $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; - - $cleanURL = "$scheme://$host$port$path$query$fragment"; - // parse_url is giving some weird results if no url and no :// is given, - // so let's test the url again - $parsedClean = parse_url($cleanURL); - if ($parsedClean === false || !isset($parsedClean['host'])) { - return null; + $localVobject = Reader::read($calendarObject['calendardata']); + foreach ($localVobject->getComponents() as $component) { + unset($component->{'DTSTAMP'}); } - return $cleanURL; - } - - /** - * Returns a random uri for a calendar-object - * - * @return string - */ - public function getRandomCalendarObjectUri():string { - return UUIDUtil::getUUID() . '.ics'; + return strcasecmp($localVobject->serialize(), $vObject->serialize()) === 0; } } diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php index f61fb5d2f0a..f9bad25bf31 100644 --- a/apps/dav/lib/Capabilities.php +++ b/apps/dav/lib/Capabilities.php @@ -1,51 +1,39 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH - * - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Louis Chemineau <louis@chmn.me> - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @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 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV; use OCP\Capabilities\ICapability; use OCP\IConfig; +use OCP\User\IAvailabilityCoordinator; class Capabilities implements ICapability { - private IConfig $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private IConfig $config, + private IAvailabilityCoordinator $coordinator, + ) { } /** - * @return array{dav: array{chunking: string, bulkupload?: string}} + * @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}} */ public function getCapabilities() { $capabilities = [ 'dav' => [ 'chunking' => '1.0', + 'public_shares_chunking' => true, ] ]; if ($this->config->getSystemValueBool('bulkupload.enabled', true)) { $capabilities['dav']['bulkupload'] = '1.0'; } + if ($this->coordinator->isEnabled()) { + $capabilities['dav']['absence-supported'] = true; + $capabilities['dav']['absence-replacement'] = true; + } return $capabilities; } } diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php index f0a5ee05e82..b08414d3b02 100644 --- a/apps/dav/lib/CardDAV/Activity/Backend.php +++ b/apps/dav/lib/CardDAV/Activity/Backend.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -39,31 +22,13 @@ use Sabre\VObject\Reader; class Backend { - /** @var IActivityManager */ - protected $activityManager; - - /** @var IGroupManager */ - protected $groupManager; - - /** @var IUserSession */ - protected $userSession; - - /** @var IAppManager */ - protected $appManager; - - /** @var IUserManager */ - protected $userManager; - - public function __construct(IActivityManager $activityManager, - IGroupManager $groupManager, - IUserSession $userSession, - IAppManager $appManager, - IUserManager $userManager) { - $this->activityManager = $activityManager; - $this->groupManager = $groupManager; - $this->userSession = $userSession; - $this->appManager = $appManager; - $this->userManager = $userManager; + public function __construct( + protected IActivityManager $activityManager, + protected IGroupManager $groupManager, + protected IUserSession $userSession, + protected IAppManager $appManager, + protected IUserManager $userManager, + ) { } /** @@ -128,7 +93,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('addressbook', (int) $addressbookData['id']) + ->setObject('addressbook', (int)$addressbookData['id']) ->setType('contacts') ->setAuthor($currentUser); @@ -156,7 +121,7 @@ class Backend { [ 'actor' => $currentUser, 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -187,7 +152,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('addressbook', (int) $addressbookData['id']) + ->setObject('addressbook', (int)$addressbookData['id']) ->setType('contacts') ->setAuthor($currentUser); @@ -212,7 +177,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -241,7 +206,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -283,7 +248,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -310,7 +275,7 @@ class Backend { $parameters = [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -388,7 +353,7 @@ class Backend { [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $properties['id'], + 'id' => (int)$properties['id'], 'uri' => $properties['uri'], 'name' => $properties['{DAV:}displayname'], ], @@ -432,7 +397,7 @@ class Backend { $event = $this->activityManager->generateEvent(); $event->setApp('dav') - ->setObject('addressbook', (int) $addressbookData['id']) + ->setObject('addressbook', (int)$addressbookData['id']) ->setType('contacts') ->setAuthor($currentUser); @@ -444,7 +409,7 @@ class Backend { $params = [ 'actor' => $event->getAuthor(), 'addressbook' => [ - 'id' => (int) $addressbookData['id'], + 'id' => (int)$addressbookData['id'], 'uri' => $addressbookData['uri'], 'name' => $addressbookData['{DAV:}displayname'], ], @@ -471,7 +436,7 @@ class Backend { */ protected function getCardNameAndId(array $cardData): array { $vObject = Reader::read($cardData['carddata']); - return ['id' => (string) $vObject->UID, 'name' => (string) ($vObject->FN ?? '')]; + return ['id' => (string)$vObject->UID, 'name' => (string)($vObject->FN ?? '')]; } /** diff --git a/apps/dav/lib/CardDAV/Activity/Filter.php b/apps/dav/lib/CardDAV/Activity/Filter.php index 3ca4c3367d5..8b221a29ff0 100644 --- a/apps/dav/lib/CardDAV/Activity/Filter.php +++ b/apps/dav/lib/CardDAV/Activity/Filter.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -28,15 +12,10 @@ use OCP\IURLGenerator; class Filter implements IFilter { - /** @var IL10N */ - protected $l; - - /** @var IURLGenerator */ - protected $url; - - public function __construct(IL10N $l, IURLGenerator $url) { - $this->l = $l; - $this->url = $url; + public function __construct( + protected IL10N $l, + protected IURLGenerator $url, + ) { } /** @@ -55,8 +34,8 @@ class Filter implements IFilter { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. */ public function getPriority(): int { return 40; diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php index a404dde4448..cdb9769401f 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php @@ -3,28 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -43,25 +27,15 @@ class Addressbook extends Base { public const SUBJECT_UNSHARE_USER = 'addressbook_user_unshare'; public const SUBJECT_UNSHARE_GROUP = 'addressbook_group_unshare'; - /** @var IFactory */ - protected $languageFactory; - - /** @var IManager */ - protected $activityManager; - - /** @var IEventMerger */ - protected $eventMerger; - - public function __construct(IFactory $languageFactory, + public function __construct( + protected IFactory $languageFactory, IURLGenerator $url, - IManager $activityManager, + protected IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, - IEventMerger $eventMerger) { + protected IEventMerger $eventMerger, + ) { parent::__construct($userManager, $groupManager, $url); - $this->languageFactory = $languageFactory; - $this->activityManager = $activityManager; - $this->eventMerger = $eventMerger; } /** @@ -69,11 +43,11 @@ class Addressbook extends Base { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent { if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $l = $this->languageFactory->get('dav', $language); @@ -119,7 +93,7 @@ class Addressbook extends Base { } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') { $subject = $l->t('{actor} unshared address book {addressbook} from group {group}'); } else { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $parsedParameters = $this->getParameters($event, $l); diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Base.php b/apps/dav/lib/CardDAV/Activity/Provider/Base.php index f475f9d76b7..ea7680aed60 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; @@ -35,27 +18,17 @@ use OCP\IURLGenerator; use OCP\IUserManager; abstract class Base implements IProvider { - /** @var IUserManager */ - protected $userManager; - - /** @var string[] */ + /** @var string[] */ protected $userDisplayNames = []; - /** @var IGroupManager */ - protected $groupManager; - /** @var string[] */ protected $groupDisplayNames = []; - /** @var IURLGenerator */ - protected $url; - - public function __construct(IUserManager $userManager, - IGroupManager $groupManager, - IURLGenerator $urlGenerator) { - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->url = $urlGenerator; + public function __construct( + protected IUserManager $userManager, + protected IGroupManager $groupManager, + protected IURLGenerator $url, + ) { } protected function setSubjects(IEvent $event, string $subject, array $parameters): void { @@ -68,18 +41,18 @@ abstract class Base implements IProvider { * @return array */ protected function generateAddressbookParameter(array $data, IL10N $l): array { - if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI && - $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) { + if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI + && $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) { return [ 'type' => 'addressbook', - 'id' => $data['id'], + 'id' => (string)$data['id'], 'name' => $l->t('Personal'), ]; } return [ 'type' => 'addressbook', - 'id' => $data['id'], + 'id' => (string)$data['id'], 'name' => $data['name'], ]; } diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Card.php b/apps/dav/lib/CardDAV/Activity/Provider/Card.php index 7f49a428cae..acf23c00531 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Card.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Card.php @@ -3,28 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity\Provider; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -40,30 +24,16 @@ class Card extends Base { public const SUBJECT_UPDATE = 'card_update'; public const SUBJECT_DELETE = 'card_delete'; - /** @var IFactory */ - protected $languageFactory; - - /** @var IManager */ - protected $activityManager; - - /** @var IEventMerger */ - protected $eventMerger; - - /** @var IAppManager */ - protected $appManager; - - public function __construct(IFactory $languageFactory, + public function __construct( + protected IFactory $languageFactory, IURLGenerator $url, - IManager $activityManager, + protected IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, - IEventMerger $eventMerger, - IAppManager $appManager) { + protected IEventMerger $eventMerger, + protected IAppManager $appManager, + ) { parent::__construct($userManager, $groupManager, $url); - $this->languageFactory = $languageFactory; - $this->activityManager = $activityManager; - $this->eventMerger = $eventMerger; - $this->appManager = $appManager; } /** @@ -71,11 +41,11 @@ class Card extends Base { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException */ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent { if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $l = $this->languageFactory->get('dav', $language); @@ -99,7 +69,7 @@ class Card extends Base { } elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') { $subject = $l->t('You updated contact {card} in address book {addressbook}'); } else { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $parsedParameters = $this->getParameters($event, $l); diff --git a/apps/dav/lib/CardDAV/Activity/Setting.php b/apps/dav/lib/CardDAV/Activity/Setting.php index a8a83111dde..cc68cf87c83 100644 --- a/apps/dav/lib/CardDAV/Activity/Setting.php +++ b/apps/dav/lib/CardDAV/Activity/Setting.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Activity; @@ -44,8 +27,8 @@ class Setting extends CalDAVSetting { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. */ public function getPriority(): int { return 50; diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index 4f589031f06..4d30d507a7d 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -1,33 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@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\CardDAV; use OCA\DAV\DAV\Sharing\IShareable; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCP\DB\Exception; use OCP\IL10N; use OCP\Server; @@ -57,8 +37,8 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov parent::__construct($carddavBackend, $addressBookInfo); - if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME && - $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) { + if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME + && $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) { $this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Contacts'); } } @@ -253,9 +233,6 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov } public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } return parent::getChanges($syncToken, $syncLevel, $limit); } @@ -269,7 +246,12 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov } try { - return $this->carddavBackend->moveCard($sourceNode->getAddressbookId(), (int)$this->addressBookInfo['id'], $sourceNode->getUri(), $sourceNode->getOwner()); + return $this->carddavBackend->moveCard( + $sourceNode->getAddressbookId(), + $sourceNode->getUri(), + $this->getResourceId(), + $targetName, + ); } catch (Exception $e) { // Avoid injecting LoggerInterface everywhere Server::get(LoggerInterface::class)->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]); diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index 79720429479..ae77498539b 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -1,58 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arne Hamann <kontakt+github@arne.email> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Björn Schießle <bjoern@schiessle.org> - * @author call-me-matt <nextcloud@matthiasheinisch.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Thomas Citharel <nextcloud@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\CardDAV; +use OCA\DAV\Db\PropertyMapper; use OCP\Constants; -use OCP\IAddressBook; +use OCP\IAddressBookEnabled; use OCP\IURLGenerator; use Sabre\VObject\Component\VCard; use Sabre\VObject\Property; use Sabre\VObject\Reader; use Sabre\VObject\UUIDUtil; -class AddressBookImpl implements IAddressBook { - - /** @var CardDavBackend */ - private $backend; - - /** @var array */ - private $addressBookInfo; - - /** @var AddressBook */ - private $addressBook; - - /** @var IURLGenerator */ - private $urlGenerator; +class AddressBookImpl implements IAddressBookEnabled { /** * AddressBookImpl constructor. @@ -63,14 +27,13 @@ class AddressBookImpl implements IAddressBook { * @param IUrlGenerator $urlGenerator */ public function __construct( - AddressBook $addressBook, - array $addressBookInfo, - CardDavBackend $backend, - IURLGenerator $urlGenerator) { - $this->addressBook = $addressBook; - $this->addressBookInfo = $addressBookInfo; - $this->backend = $backend; - $this->urlGenerator = $urlGenerator; + private AddressBook $addressBook, + private array $addressBookInfo, + private CardDavBackend $backend, + private IURLGenerator $urlGenerator, + private PropertyMapper $propertyMapper, + private ?string $userId, + ) { } /** @@ -103,19 +66,19 @@ class AddressBookImpl implements IAddressBook { * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match * @param array $options Options to define the output format and search behavior - * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array - * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']] - * - 'escape_like_param' - If set to false wildcards _ and % are not escaped - * - 'limit' - Set a numeric limit for the search results - * - 'offset' - Set the offset for the limited search results - * - 'wildcard' - Whether the search should use wildcards + * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array + * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']] + * - 'escape_like_param' - If set to false wildcards _ and % are not escaped + * - 'limit' - Set a numeric limit for the search results + * - 'offset' - Set the offset for the limited search results + * - 'wildcard' - Whether the search should use wildcards * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs - * example result: - * [ - * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'], - * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']] - * ] + * example result: + * [ + * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'], + * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']] + * ] * @since 5.0.0 */ public function search($pattern, $searchProperties, $options) { @@ -156,13 +119,13 @@ class AddressBookImpl implements IAddressBook { if (is_string($entry)) { $property = $vCard->createProperty($key, $entry); } else { - if (($key === "ADR" || $key === "PHOTO") && is_string($entry["value"])) { - $entry["value"] = stripslashes($entry["value"]); - $entry["value"] = explode(';', $entry["value"]); + if (($key === 'ADR' || $key === 'PHOTO') && is_string($entry['value'])) { + $entry['value'] = stripslashes($entry['value']); + $entry['value'] = explode(';', $entry['value']); } - $property = $vCard->createProperty($key, $entry["value"]); - if (isset($entry["type"])) { - $property->add('TYPE', $entry["type"]); + $property = $vCard->createProperty($key, $entry['value']); + if (isset($entry['type'])) { + $property->add('TYPE', $entry['type']); } } $vCard->add($property); @@ -189,6 +152,10 @@ class AddressBookImpl implements IAddressBook { $permissions = $this->addressBook->getACL(); $result = 0; foreach ($permissions as $permission) { + if ($this->addressBookInfo['principaluri'] !== $permission['principal']) { + continue; + } + switch ($permission['privilege']) { case '{DAV:}read': $result |= Constants::PERMISSION_READ; @@ -344,8 +311,29 @@ class AddressBookImpl implements IAddressBook { */ public function isSystemAddressBook(): bool { return $this->addressBookInfo['principaluri'] === 'principals/system/system' && ( - $this->addressBookInfo['uri'] === 'system' || - $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl() + $this->addressBookInfo['uri'] === 'system' + || $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl() ); } + + public function isEnabled(): bool { + if (!$this->userId) { + return true; + } + + if ($this->isSystemAddressBook()) { + $user = $this->userId ; + $uri = 'z-server-generated--system'; + } else { + $user = str_replace('principals/users/', '', $this->addressBookInfo['principaluri']); + $uri = $this->addressBookInfo['uri']; + } + + $path = 'addressbooks/users/' . $user . '/' . $uri; + $properties = $this->propertyMapper->findPropertyByPathAndName($user, $path, '{http://owncloud.org/ns}enabled'); + if (count($properties) > 0) { + return (bool)$properties[0]->getPropertyvalue(); + } + return true; + } } diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php index f450dbe2ef9..5679a03545e 100644 --- a/apps/dav/lib/CardDAV/AddressBookRoot.php +++ b/apps/dav/lib/CardDAV/AddressBookRoot.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @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\CardDAV; @@ -30,26 +13,20 @@ use OCP\IUser; class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { - /** @var PluginManager */ - private $pluginManager; - private ?IUser $user; - private ?IGroupManager $groupManager; - /** * @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend * @param \Sabre\CardDAV\Backend\BackendInterface $carddavBackend * @param string $principalPrefix */ - public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, + public function __construct( + \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, \Sabre\CardDAV\Backend\BackendInterface $carddavBackend, - PluginManager $pluginManager, - ?IUser $user, - ?IGroupManager $groupManager, - string $principalPrefix = 'principals') { + private PluginManager $pluginManager, + private ?IUser $user, + private ?IGroupManager $groupManager, + string $principalPrefix = 'principals', + ) { parent::__construct($principalBackend, $carddavBackend, $principalPrefix); - $this->pluginManager = $pluginManager; - $this->user = $user; - $this->groupManager = $groupManager; } /** diff --git a/apps/dav/lib/CardDAV/Card.php b/apps/dav/lib/CardDAV/Card.php index 093255392e0..8cd4fd7e5ee 100644 --- a/apps/dav/lib/CardDAV/Card.php +++ b/apps/dav/lib/CardDAV/Card.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023, Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @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: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\CardDAV; class Card extends \Sabre\CardDAV\Card { public function getId(): int { - return (int) $this->cardData['id']; + return (int)$this->cardData['id']; } public function getUri(): string { diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index f887e3b32b7..a78686eb61d 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -1,37 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arne Hamann <kontakt+github@arne.email> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Chih-Hsuan Yen <yan12125@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author matt <34400929+call-me-matt@users.noreply.github.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @author Thomas Citharel <nextcloud@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\CardDAV; @@ -51,6 +23,7 @@ use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; use PDO; @@ -87,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { private IUserManager $userManager, private IEventDispatcher $dispatcher, private Sharing\Backend $sharingBackend, + private IConfig $config, ) { } @@ -104,7 +78,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); $result = $query->executeQuery(); - $column = (int) $result->fetchOne(); + $column = (int)$result->fetchOne(); $result->closeCursor(); return $column; } @@ -155,7 +129,6 @@ class CardDavBackend implements BackendInterface, SyncSupport { // query for shared addressbooks $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); $principals[] = $principalUri; @@ -188,8 +161,8 @@ class CardDavBackend implements BackendInterface, SyncSupport { // New share can not have more permissions then the old one. continue; } - if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) && - $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { + if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) + && $addressBooks[$row['id']][$readOnlyPropertyName] === 0) { // Old share is already read-write, no more permissions can be gained continue; } @@ -228,7 +201,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $addressBooks = []; - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $addressBooks[$row['id']] = [ 'id' => $row['id'], @@ -358,7 +331,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) ->executeStatement(); - $this->addChange($addressBookId, "", 2); + $this->addChange($addressBookId, '', 2); $addressBookRow = $this->getAddressBookById((int)$addressBookId); $shares = $this->getShares((int)$addressBookId); @@ -379,6 +352,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param array $properties * @return int * @throws BadRequest + * @throws Exception */ public function createAddressBook($principalUri, $url, array $properties) { if (strlen($url) > 255) { @@ -423,7 +397,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'synctoken' => $query->createParameter('synctoken'), ]) ->setParameters($values) - ->execute(); + ->executeStatement(); $addressBookId = $query->getLastInsertId(); return [ @@ -444,7 +418,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return void */ public function deleteAddressBook($addressBookId) { - $this->atomic(function () use ($addressBookId) { + $this->atomic(function () use ($addressBookId): void { $addressBookId = (int)$addressBookId; $addressBookData = $this->getAddressBookById($addressBookId); $shares = $this->getShares($addressBookId); @@ -501,13 +475,13 @@ class CardDavBackend implements BackendInterface, SyncSupport { */ public function getCards($addressbookId) { $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) + $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) ->from($this->dbCardsTable) ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressbookId))); $cards = []; - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $row['etag'] = '"' . $row['etag'] . '"'; @@ -538,13 +512,13 @@ class CardDavBackend implements BackendInterface, SyncSupport { */ public function getCard($addressBookId, $cardUri) { $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) + $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) ->from($this->dbCardsTable) ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) ->setMaxResults(1); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); if (!$row) { return false; @@ -581,14 +555,14 @@ class CardDavBackend implements BackendInterface, SyncSupport { $cards = []; $query = $this->db->getQueryBuilder(); - $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) + $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid']) ->from($this->dbCardsTable) ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))); foreach ($chunks as $uris) { $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $row['etag'] = '"' . $row['etag'] . '"'; @@ -662,7 +636,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'etag' => $query->createNamedParameter($etag), 'uid' => $query->createNamedParameter($uid), ]) - ->execute(); + ->executeStatement(); $etagCacheKey = "$addressBookId#$cardUri"; $this->etagCache[$etagCacheKey] = $etag; @@ -725,7 +699,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->set('uid', $query->createNamedParameter($uid)) ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri))) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->execute(); + ->executeStatement(); $this->etagCache[$etagCacheKey] = $etag; @@ -743,32 +717,33 @@ class CardDavBackend implements BackendInterface, SyncSupport { /** * @throws Exception */ - public function moveCard(int $sourceAddressBookId, int $targetAddressBookId, string $cardUri, string $oldPrincipalUri): bool { - return $this->atomic(function () use ($sourceAddressBookId, $targetAddressBookId, $cardUri, $oldPrincipalUri) { - $card = $this->getCard($sourceAddressBookId, $cardUri); + public function moveCard(int $sourceAddressBookId, string $sourceObjectUri, int $targetAddressBookId, string $tragetObjectUri): bool { + return $this->atomic(function () use ($sourceAddressBookId, $sourceObjectUri, $targetAddressBookId, $tragetObjectUri) { + $card = $this->getCard($sourceAddressBookId, $sourceObjectUri); if (empty($card)) { return false; } + $sourceObjectId = (int)$card['id']; $query = $this->db->getQueryBuilder(); $query->update('cards') ->set('addressbookid', $query->createNamedParameter($targetAddressBookId, IQueryBuilder::PARAM_INT)) - ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)) + ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR)) + ->where($query->expr()->eq('uri', $query->createNamedParameter($sourceObjectUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($sourceAddressBookId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) ->executeStatement(); - $this->purgeProperties($sourceAddressBookId, (int)$card['id']); - $this->updateProperties($sourceAddressBookId, $card['uri'], $card['carddata']); + $this->purgeProperties($sourceAddressBookId, $sourceObjectId); + $this->updateProperties($targetAddressBookId, $tragetObjectUri, $card['carddata']); - $this->addChange($sourceAddressBookId, $card['uri'], 3); - $this->addChange($targetAddressBookId, $card['uri'], 1); + $this->addChange($sourceAddressBookId, $sourceObjectUri, 3); + $this->addChange($targetAddressBookId, $tragetObjectUri, 1); - $card = $this->getCard($targetAddressBookId, $cardUri); + $card = $this->getCard($targetAddressBookId, $tragetObjectUri); // Card wasn't found - possibly because it was deleted in the meantime by a different client if (empty($card)) { return false; } - $targetAddressBookRow = $this->getAddressBookById($targetAddressBookId); // the address book this card is being moved to does not exist any longer if (empty($targetAddressBookRow)) { @@ -878,6 +853,8 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return array */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + $maxLimit = $this->config->getSystemValueInt('carddav_sync_request_truncation', 2500); + $limit = ($limit === null) ? $maxLimit : min($limit, $maxLimit); // Current synctoken return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) { $qb = $this->db->getQueryBuilder(); @@ -900,10 +877,35 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'modified' => [], 'deleted' => [], ]; - - if ($syncToken) { + if (str_starts_with($syncToken, 'init_')) { + $syncValues = explode('_', $syncToken); + $lastID = $syncValues[1]; + $initialSyncToken = $syncValues[2]; + $qb = $this->db->getQueryBuilder(); + $qb->select('id', 'uri') + ->from('cards') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)), + $qb->expr()->gt('id', $qb->createNamedParameter($lastID))) + )->orderBy('id') + ->setMaxResults($limit); + $stmt = $qb->executeQuery(); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if (count($values) === 0) { + $result['syncToken'] = $initialSyncToken; + $result['result_truncated'] = false; + $result['added'] = []; + } else { + $lastID = $values[array_key_last($values)]['id']; + $result['added'] = array_column($values, 'uri'); + $result['syncToken'] = count($result['added']) >= $limit ? "init_{$lastID}_$initialSyncToken" : $initialSyncToken ; + $result['result_truncated'] = count($result['added']) >= $limit; + } + } elseif ($syncToken) { $qb = $this->db->getQueryBuilder(); - $qb->select('uri', 'operation') + $qb->select('uri', 'operation', 'synctoken') ->from('addressbookchanges') ->where( $qb->expr()->andX( @@ -913,22 +915,31 @@ class CardDavBackend implements BackendInterface, SyncSupport { ) )->orderBy('synctoken'); - if (is_int($limit) && $limit > 0) { + if ($limit > 0) { $qb->setMaxResults($limit); } // Fetching all changes $stmt = $qb->executeQuery(); + $rowCount = $stmt->rowCount(); $changes = []; + $highestSyncToken = 0; // This loop ensures that any duplicates are overwritten, only the // last change on a node is relevant. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $changes[$row['uri']] = $row['operation']; + $highestSyncToken = $row['synctoken']; } + $stmt->closeCursor(); + // No changes found, use current token + if (empty($changes)) { + $result['syncToken'] = $currentToken; + } + foreach ($changes as $uri => $operation) { switch ($operation) { case 1: @@ -942,16 +953,43 @@ class CardDavBackend implements BackendInterface, SyncSupport { break; } } + + /* + * The synctoken in oc_addressbooks is always the highest synctoken in oc_addressbookchanges for a given addressbook plus one (see addChange). + * + * For truncated results, it is expected that we return the highest token from the response, so the client can continue from the latest change. + * + * For non-truncated results, it is expected to return the currentToken. If we return the highest token, as with truncated results, the client will always think it is one change behind. + * + * Therefore, we differentiate between truncated and non-truncated results when returning the synctoken. + */ + if ($rowCount === $limit && $highestSyncToken < $currentToken) { + $result['syncToken'] = $highestSyncToken; + $result['result_truncated'] = true; + } } else { $qb = $this->db->getQueryBuilder(); - $qb->select('uri') + $qb->select('id', 'uri') ->from('cards') ->where( $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) ); // No synctoken supplied, this is the initial sync. + $qb->setMaxResults($limit); $stmt = $qb->executeQuery(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + if (empty($values)) { + $result['added'] = []; + return $result; + } + $lastID = $values[array_key_last($values)]['id']; + if (count($values) >= $limit) { + $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken; + $result['result_truncated'] = true; + } + + $result['added'] = array_column($values, 'uri'); + $stmt->closeCursor(); } return $result; @@ -967,7 +1005,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return void */ protected function addChange(int $addressBookId, string $objectUri, int $operation): void { - $this->atomic(function () use ($addressBookId, $objectUri, $operation) { + $this->atomic(function () use ($addressBookId, $objectUri, $operation): void { $query = $this->db->getQueryBuilder(); $query->select('synctoken') ->from('addressbooks') @@ -1042,7 +1080,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param list<string> $remove */ public function updateShares(IShareable $shareable, array $add, array $remove): void { - $this->atomic(function () use ($shareable, $add, $remove) { + $this->atomic(function () use ($shareable, $add, $remove): void { $addressBookId = $shareable->getResourceId(); $addressBookData = $this->getAddressBookById($addressBookId); $oldShares = $this->getShares($addressBookId); @@ -1060,11 +1098,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match * @param array $options = array() to define the search behavior - * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array - * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are - * - 'limit' - Set a numeric limit for the search results - * - 'offset' - Set the offset for the limited search results - * - 'wildcard' - Whether the search should use wildcards + * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array + * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are + * - 'limit' - Set a numeric limit for the search results + * - 'offset' - Set the offset for the limited search results + * - 'wildcard' - Whether the search should use wildcards * @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options * @return array an array of contacts which are arrays of key-value-pairs */ @@ -1089,7 +1127,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { array $options = []): array { return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) { $addressBookIds = array_map(static function ($row):int { - return (int) $row['id']; + return (int)$row['id']; }, $this->getAddressBooksForUser($principalUri)); return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options); @@ -1176,24 +1214,24 @@ class CardDavBackend implements BackendInterface, SyncSupport { /** * FIXME Find a way to match only 4 last digits * BDAY can be --1018 without year or 20001019 with it - * $bDayOr = $query2->expr()->orX(); + * $bDayOr = []; * if ($options['since'] instanceof DateTimeFilter) { - * $bDayOr->add( + * $bDayOr[] = * $query2->expr()->gte('SUBSTR(cp_bday.value, -4)', - * $query2->createNamedParameter($options['since']->get()->format('md'))) + * $query2->createNamedParameter($options['since']->get()->format('md')) * ); * } * if ($options['until'] instanceof DateTimeFilter) { - * $bDayOr->add( + * $bDayOr[] = * $query2->expr()->lte('SUBSTR(cp_bday.value, -4)', - * $query2->createNamedParameter($options['until']->get()->format('md'))) + * $query2->createNamedParameter($options['until']->get()->format('md')) * ); * } - * $query2->andWhere($bDayOr); + * $query2->andWhere($query2->expr()->orX(...$bDayOr)); */ } - $result = $query2->execute(); + $result = $query2->executeQuery(); $matches = $result->fetchAll(); $result->closeCursor(); $matches = array_map(function ($match) { @@ -1214,7 +1252,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { } return array_map(function ($array) { - $array['addressbookid'] = (int) $array['addressbookid']; + $array['addressbookid'] = (int)$array['addressbookid']; $modified = false; $array['carddata'] = $this->readBlob($array['carddata'], $modified); if ($modified) { @@ -1235,7 +1273,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->from($this->dbCardsPropertiesTable) ->where($query->expr()->eq('name', $query->createNamedParameter($name))) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId))) - ->execute(); + ->executeQuery(); $all = $result->fetchAll(PDO::FETCH_COLUMN); $result->closeCursor(); @@ -1255,7 +1293,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->where($query->expr()->eq('id', $query->createParameter('id'))) ->setParameter('id', $id); - $result = $query->execute(); + $result = $query->executeQuery(); $uri = $result->fetch(); $result->closeCursor(); @@ -1279,7 +1317,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query->select('*')->from($this->dbCardsTable) ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - $queryResult = $query->execute(); + $queryResult = $query->executeQuery(); $contact = $queryResult->fetch(); $queryResult->closeCursor(); @@ -1320,7 +1358,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param string $vCardSerialized */ protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) { - $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized) { + $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized): void { $cardId = $this->getCardId($addressBookId, $cardUri); $vCard = $this->readCard($vCardSerialized); @@ -1352,7 +1390,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query->setParameter('name', $property->name); $query->setParameter('value', mb_strcut($property->getValue(), 0, 254)); $query->setParameter('preferred', $preferred); - $query->execute(); + $query->executeStatement(); } }, $this->db); } @@ -1378,7 +1416,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query->delete($this->dbCardsPropertiesTable) ->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId))) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - $query->execute(); + $query->executeStatement(); } /** @@ -1390,7 +1428,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) ->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))); - $result = $query->execute(); + $result = $query->executeQuery(); $cardIds = $result->fetch(); $result->closeCursor(); @@ -1426,7 +1464,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ->from('addressbookchanges'); $result = $query->executeQuery(); - $maxId = (int) $result->fetchOne(); + $maxId = (int)$result->fetchOne(); $result->closeCursor(); if (!$maxId || $maxId < $keep) { return 0; diff --git a/apps/dav/lib/CardDAV/ContactsManager.php b/apps/dav/lib/CardDAV/ContactsManager.php index bed1e676337..b35137c902d 100644 --- a/apps/dav/lib/CardDAV/ContactsManager.php +++ b/apps/dav/lib/CardDAV/ContactsManager.php @@ -1,50 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobia De Koninck <tobia@ledfan.be> - * - * @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\CardDAV; +use OCA\DAV\Db\PropertyMapper; use OCP\Contacts\IManager; use OCP\IL10N; use OCP\IURLGenerator; class ContactsManager { - /** @var CardDavBackend */ - private $backend; - - /** @var IL10N */ - private $l10n; - /** * ContactsManager constructor. * * @param CardDavBackend $backend * @param IL10N $l10n */ - public function __construct(CardDavBackend $backend, IL10N $l10n) { - $this->backend = $backend; - $this->l10n = $l10n; + public function __construct( + private CardDavBackend $backend, + private IL10N $l10n, + private PropertyMapper $propertyMapper, + ) { } /** @@ -54,33 +33,37 @@ class ContactsManager { */ public function setupContactsProvider(IManager $cm, $userId, IURLGenerator $urlGenerator) { $addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId"); - $this->register($cm, $addressBooks, $urlGenerator); - $this->setupSystemContactsProvider($cm, $urlGenerator); + $this->register($cm, $addressBooks, $urlGenerator, $userId); + $this->setupSystemContactsProvider($cm, $userId, $urlGenerator); } /** * @param IManager $cm + * @param ?string $userId * @param IURLGenerator $urlGenerator */ - public function setupSystemContactsProvider(IManager $cm, IURLGenerator $urlGenerator) { - $addressBooks = $this->backend->getAddressBooksForUser("principals/system/system"); - $this->register($cm, $addressBooks, $urlGenerator); + public function setupSystemContactsProvider(IManager $cm, ?string $userId, IURLGenerator $urlGenerator) { + $addressBooks = $this->backend->getAddressBooksForUser('principals/system/system'); + $this->register($cm, $addressBooks, $urlGenerator, $userId); } /** * @param IManager $cm * @param $addressBooks * @param IURLGenerator $urlGenerator + * @param ?string $userId */ - private function register(IManager $cm, $addressBooks, $urlGenerator) { + private function register(IManager $cm, $addressBooks, $urlGenerator, ?string $userId) { foreach ($addressBooks as $addressBookInfo) { - $addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo, $this->l10n); + $addressBook = new AddressBook($this->backend, $addressBookInfo, $this->l10n); $cm->registerAddressBook( new AddressBookImpl( $addressBook, $addressBookInfo, $this->backend, - $urlGenerator + $urlGenerator, + $this->propertyMapper, + $userId, ) ); } diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php index 8ea75fbef74..30dba99839e 100644 --- a/apps/dav/lib/CardDAV/Converter.php +++ b/apps/dav/lib/CardDAV/Converter.php @@ -1,53 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\CardDAV; +use DateTimeImmutable; use Exception; use OCP\Accounts\IAccountManager; use OCP\IImage; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use Psr\Log\LoggerInterface; use Sabre\VObject\Component\VCard; use Sabre\VObject\Property\Text; +use Sabre\VObject\Property\VCard\Date; class Converter { - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IAccountManager */ - private $accountManager; - private IUserManager $userManager; - - public function __construct(IAccountManager $accountManager, - IUserManager $userManager, IURLGenerator $urlGenerator) { - $this->accountManager = $accountManager; - $this->userManager = $userManager; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IAccountManager $accountManager, + private IUserManager $userManager, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + ) { } public function createCardFromUser(IUser $user): ?VCard { @@ -98,7 +76,7 @@ class Converter { new Text( $vCard, 'X-SOCIALPROFILE', - $this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $user->getUID()]), + $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]), [ 'TYPE' => 'NEXTCLOUD', 'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED @@ -134,6 +112,24 @@ class Converter { case IAccountManager::PROPERTY_ROLE: $vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope])); break; + case IAccountManager::PROPERTY_BIOGRAPHY: + $vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope])); + break; + case IAccountManager::PROPERTY_BIRTHDATE: + try { + $birthdate = new DateTimeImmutable($property->getValue()); + } catch (Exception $e) { + // Invalid date -> just skip the property + $this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [ + 'exception' => $e, + 'userId' => $user->getUID(), + ]); + break; + } + $dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]); + $dateProperty->setDateTime($birthdate); + $vCard->add($dateProperty); + break; } } diff --git a/apps/dav/lib/CardDAV/HasPhotoPlugin.php b/apps/dav/lib/CardDAV/HasPhotoPlugin.php index 310649bdae9..6e2e0423910 100644 --- a/apps/dav/lib/CardDAV/HasPhotoPlugin.php +++ b/apps/dav/lib/CardDAV/HasPhotoPlugin.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php index 3ebc91e5533..74a8b032e42 100644 --- a/apps/dav/lib/CardDAV/ImageExportPlugin.php +++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php @@ -1,29 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jacob Neplokh <me@jacobneplokh.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\CardDAV; +use OCP\AppFramework\Http; use OCP\Files\NotFoundException; use Sabre\CardDAV\Card; use Sabre\DAV\Server; @@ -35,16 +19,15 @@ class ImageExportPlugin extends ServerPlugin { /** @var Server */ protected $server; - /** @var PhotoCache */ - private $cache; /** * ImageExportPlugin constructor. * * @param PhotoCache $cache */ - public function __construct(PhotoCache $cache) { - $this->cache = $cache; + public function __construct( + private PhotoCache $cache, + ) { } /** @@ -77,7 +60,7 @@ class ImageExportPlugin extends ServerPlugin { $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); - if (!($node instanceof Card)) { + if (!$node instanceof Card) { return true; } @@ -104,11 +87,11 @@ class ImageExportPlugin extends ServerPlugin { $response->setHeader('Content-Type', $file->getMimeType()); $fileName = $node->getName() . '.' . PhotoCache::ALLOWED_CONTENT_TYPES[$file->getMimeType()]; $response->setHeader('Content-Disposition', "attachment; filename=$fileName"); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setBody($file->getContent()); } catch (NotFoundException $e) { - $response->setStatus(404); + $response->setStatus(Http::STATUS_NO_CONTENT); } return false; diff --git a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php index 6f05bb6ce6a..372906a6ae8 100644 --- a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php +++ b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Integration; @@ -50,12 +32,10 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties { */ private const DELIMITER = '--'; - private string $appId; - private string $uri; - - public function __construct(string $appId, string $uri) { - $this->appId = $appId; - $this->uri = $uri; + public function __construct( + private string $appId, + private string $uri, + ) { } /** diff --git a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php index 0560c13c05c..a8fa074f635 100644 --- a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php +++ b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Integration; diff --git a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php index a56faad8413..9d6b0df838e 100644 --- a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php +++ b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php @@ -3,29 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; +use OCP\AppFramework\Http; use Sabre\DAV; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; @@ -61,7 +44,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin { } // Only handling xml - $contentType = (string) $response->getHeader('Content-Type'); + $contentType = (string)$response->getHeader('Content-Type'); if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) { return; } @@ -83,7 +66,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin { $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); $response->setHeader('Content-Type', 'text/vcard'); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setBody($output); return true; diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php index 9f05ec2354a..03c71f7e4a3 100644 --- a/apps/dav/lib/CardDAV/PhotoCache.php +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -1,39 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Jacob Neplokh <me@jacobneplokh.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\DAV\CardDAV; +use OCP\Files\AppData\IAppDataFactory; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Image; use Psr\Log\LoggerInterface; use Sabre\CardDAV\Card; use Sabre\VObject\Document; @@ -42,24 +22,22 @@ use Sabre\VObject\Property\Binary; use Sabre\VObject\Reader; class PhotoCache { + private ?IAppData $photoCacheAppData = null; - /** @var array */ + /** @var array */ public const ALLOWED_CONTENT_TYPES = [ 'image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'image/vnd.microsoft.icon' => 'ico', + 'image/webp' => 'webp', + 'image/avif' => 'avif', ]; - protected IAppData $appData; - protected LoggerInterface $logger; - - /** - * PhotoCache constructor. - */ - public function __construct(IAppData $appData, LoggerInterface $logger) { - $this->appData = $appData; - $this->logger = $logger; + public function __construct( + private IAppDataFactory $appDataFactory, + private LoggerInterface $logger, + ) { } /** @@ -133,7 +111,7 @@ class PhotoCache { throw new NotFoundException; } - $photo = new \OCP\Image(); + $photo = new Image(); /** @var ISimpleFile $file */ $file = $folder->getFile('photo.' . $ext); $photo->loadFromData($file->getContent()); @@ -143,7 +121,7 @@ class PhotoCache { $ratio = 1 / $ratio; } - $size = (int) ($size * $ratio); + $size = (int)($size * $ratio); if ($size !== -1) { $photo->resize($size); } @@ -165,13 +143,12 @@ class PhotoCache { private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder { $hash = md5($addressBookId . ' ' . $cardUri); try { - return $this->appData->getFolder($hash); + return $this->getPhotoCacheAppData()->getFolder($hash); } catch (NotFoundException $e) { if ($createIfNotExists) { - return $this->appData->newFolder($hash); - } else { - throw $e; + return $this->getPhotoCacheAppData()->newFolder($hash); } + throw $e; } } @@ -264,7 +241,7 @@ class PhotoCache { if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) { /** @var Parameter $typeParam */ $typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE']; - $type = (string) $typeParam->getValue(); + $type = (string)$typeParam->getValue(); if (str_starts_with($type, 'image/')) { return $type; @@ -288,4 +265,11 @@ class PhotoCache { // that's OK, nothing to do } } + + private function getPhotoCacheAppData(): IAppData { + if ($this->photoCacheAppData === null) { + $this->photoCacheAppData = $this->appDataFactory->get('dav-photocache'); + } + return $this->photoCacheAppData; + } } diff --git a/apps/dav/lib/CardDAV/Plugin.php b/apps/dav/lib/CardDAV/Plugin.php index df8f7e6a436..0ec10306ceb 100644 --- a/apps/dav/lib/CardDAV/Plugin.php +++ b/apps/dav/lib/CardDAV/Plugin.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.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\CardDAV; diff --git a/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php new file mode 100644 index 00000000000..3e18a1341b0 --- /dev/null +++ b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\CardDAV\Security; + +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Connector\Sabre\Exception\TooManyRequests; +use OCP\IAppConfig; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +use Sabre\DAV; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\ServerPlugin; +use function count; +use function explode; + +class CardDavRateLimitingPlugin extends ServerPlugin { + public function __construct( + private Limiter $limiter, + private IUserManager $userManager, + private CardDavBackend $cardDavBackend, + private LoggerInterface $logger, + private IAppConfig $config, + private ?string $userId, + ) { + $this->limiter = $limiter; + $this->userManager = $userManager; + $this->cardDavBackend = $cardDavBackend; + $this->config = $config; + $this->logger = $logger; + } + + public function initialize(DAV\Server $server): void { + $server->on('beforeBind', [$this, 'beforeBind'], 1); + } + + public function beforeBind(string $path): void { + if ($this->userId === null) { + // We only care about authenticated users here + return; + } + $user = $this->userManager->get($this->userId); + if ($user === null) { + // We only care about authenticated users here + return; + } + + $pathParts = explode('/', $path); + if (count($pathParts) === 4 && $pathParts[0] === 'addressbooks') { + // Path looks like addressbooks/users/username/addressbooksname so a new addressbook is created + try { + $this->limiter->registerUserRequest( + 'carddav-create-address-book', + $this->config->getValueInt('dav', 'rateLimitAddressBookCreation', 10), + $this->config->getValueInt('dav', 'rateLimitPeriodAddressBookCreation', 3600), + $user + ); + } catch (RateLimitExceededException $e) { + throw new TooManyRequests('Too many addressbooks created', 0, $e); + } + + $addressBookLimit = $this->config->getValueInt('dav', 'maximumAdressbooks', 10); + if ($addressBookLimit === -1) { + return; + } + $numAddressbooks = $this->cardDavBackend->getAddressBooksForUserCount('principals/users/' . $user->getUID()); + + if ($numAddressbooks >= $addressBookLimit) { + $this->logger->warning('Maximum number of address books reached', [ + 'addressbooks' => $numAddressbooks, + 'addressBookLimit' => $addressBookLimit, + ]); + throw new Forbidden('AddressBook limit reached', 0); + } + } + } + +} diff --git a/apps/dav/lib/CardDAV/Sharing/Backend.php b/apps/dav/lib/CardDAV/Sharing/Backend.php index f0f53ba9cfa..557115762fc 100644 --- a/apps/dav/lib/CardDAV/Sharing/Backend.php +++ b/apps/dav/lib/CardDAV/Sharing/Backend.php @@ -2,22 +2,8 @@ declare(strict_types=1); /** - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Sharing; @@ -30,7 +16,8 @@ use OCP\IUserManager; use Psr\Log\LoggerInterface; class Backend extends SharingBackend { - public function __construct(private IUserManager $userManager, + public function __construct( + private IUserManager $userManager, private IGroupManager $groupManager, private Principal $principalBackend, private ICacheFactory $cacheFactory, diff --git a/apps/dav/lib/CardDAV/Sharing/Service.php b/apps/dav/lib/CardDAV/Sharing/Service.php index 5da71defb5e..1ab208f7ec3 100644 --- a/apps/dav/lib/CardDAV/Sharing/Service.php +++ b/apps/dav/lib/CardDAV/Sharing/Service.php @@ -2,22 +2,8 @@ declare(strict_types=1); /** - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV\Sharing; @@ -27,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService; class Service extends SharingService { protected string $resourceType = 'addressbook'; - public function __construct(protected SharingMapper $mapper) { + public function __construct( + protected SharingMapper $mapper, + ) { parent::__construct($mapper); } } diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 01747a9b105..e6da3ed5923 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -1,77 +1,53 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @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\CardDAV; use OCP\AppFramework\Db\TTransactional; use OCP\AppFramework\Http; +use OCP\DB\Exception; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; +use Psr\Http\Client\ClientExceptionInterface; use Psr\Log\LoggerInterface; -use Sabre\DAV\Client; use Sabre\DAV\Xml\Response\MultiStatus; use Sabre\DAV\Xml\Service; -use Sabre\HTTP\ClientHttpException; use Sabre\VObject\Reader; +use Sabre\Xml\ParseException; use function is_null; class SyncService { use TTransactional; - - private CardDavBackend $backend; - private IUserManager $userManager; - private IDBConnection $dbConnection; - private LoggerInterface $logger; private ?array $localSystemAddressBook = null; - private Converter $converter; protected string $certPath; - public function __construct(CardDavBackend $backend, - IUserManager $userManager, - IDBConnection $dbConnection, - LoggerInterface $logger, - Converter $converter) { - $this->backend = $backend; - $this->userManager = $userManager; - $this->logger = $logger; - $this->converter = $converter; + public function __construct( + private CardDavBackend $backend, + private IUserManager $userManager, + private IDBConnection $dbConnection, + private LoggerInterface $logger, + private Converter $converter, + private IClientService $clientService, + private IConfig $config, + ) { $this->certPath = ''; - $this->dbConnection = $dbConnection; } /** + * @psalm-return list{0: ?string, 1: boolean} * @throws \Exception */ - public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string { + public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): array { // 1. create addressbook $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties); $addressBookId = $book['id']; @@ -79,7 +55,7 @@ class SyncService { // 2. query changes try { $response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken); - } catch (ClientHttpException $ex) { + } catch (ClientExceptionInterface $ex) { if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) { // remote server revoked access to the address book, remove it $this->backend->deleteAddressBook($addressBookId); @@ -96,12 +72,12 @@ class SyncService { $cardUri = basename($resource); if (isset($status[200])) { $vCard = $this->download($url, $userName, $sharedSecret, $resource); - $this->atomic(function () use ($addressBookId, $cardUri, $vCard) { + $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void { $existingCard = $this->backend->getCard($addressBookId, $cardUri); if ($existingCard === false) { - $this->backend->createCard($addressBookId, $cardUri, $vCard['body']); + $this->backend->createCard($addressBookId, $cardUri, $vCard); } else { - $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']); + $this->backend->updateCard($addressBookId, $cardUri, $vCard); } }, $this->dbConnection); } else { @@ -109,82 +85,134 @@ class SyncService { } } - return $response['token']; + return [ + $response['token'], + $response['truncated'], + ]; } /** * @throws \Sabre\DAV\Exception\BadRequest */ public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array { - return $this->atomic(function () use ($principal, $uri, $properties) { - $book = $this->backend->getAddressBooksByUri($principal, $uri); - if (!is_null($book)) { - return $book; - } - $this->backend->createAddressBook($principal, $uri, $properties); - - return $this->backend->getAddressBooksByUri($principal, $uri); - }, $this->dbConnection); - } + try { + return $this->atomic(function () use ($principal, $uri, $properties) { + $book = $this->backend->getAddressBooksByUri($principal, $uri); + if (!is_null($book)) { + return $book; + } + $this->backend->createAddressBook($principal, $uri, $properties); - /** - * Check if there is a valid certPath we should use - */ - protected function getCertPath(): string { + return $this->backend->getAddressBooksByUri($principal, $uri); + }, $this->dbConnection); + } catch (Exception $e) { + // READ COMMITTED doesn't prevent a nonrepeatable read above, so + // two processes might create an address book here. Ignore our + // failure and continue loading the entry written by the other process + if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + throw $e; + } - // we already have a valid certPath - if ($this->certPath !== '') { - return $this->certPath; + // If this fails we might have hit a replication node that does not + // have the row written in the other process. + // TODO: find an elegant way to handle this + $ab = $this->backend->getAddressBooksByUri($principal, $uri); + if ($ab === null) { + throw new Exception('Could not create system address book', $e->getCode(), $e); + } + return $ab; } + } - $certManager = \OC::$server->getCertificateManager(); - $certPath = $certManager->getAbsoluteBundlePath(); - if (file_exists($certPath)) { - $this->certPath = $certPath; - } + public function ensureLocalSystemAddressBookExists(): ?array { + return $this->ensureSystemAddressBookExists('principals/system/system', 'system', [ + '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance' + ]); + } - return $this->certPath; + private function prepareUri(string $host, string $path): string { + /* + * The trailing slash is important for merging the uris. + * + * $host is stored in oc_trusted_servers.url and usually without a trailing slash. + * + * Example for a report request + * + * $host = 'https://server.internal/cloud' + * $path = 'remote.php/dav/addressbooks/system/system/system' + * + * Without the trailing slash, the webroot is missing: + * https://server.internal/remote.php/dav/addressbooks/system/system/system + * + * Example for a download request + * + * $host = 'https://server.internal/cloud' + * $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf' + * + * The response from the remote usually contains the webroot already and must be normalized to: + * https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf + */ + $host = rtrim($host, '/') . '/'; + + $uri = \GuzzleHttp\Psr7\UriResolver::resolve( + \GuzzleHttp\Psr7\Utils::uriFor($host), + \GuzzleHttp\Psr7\Utils::uriFor($path) + ); + + return (string)$uri; } - protected function getClient(string $url, string $userName, string $sharedSecret): Client { - $settings = [ - 'baseUri' => $url . '/', - 'userName' => $userName, - 'password' => $sharedSecret, + /** + * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool} + * @throws ClientExceptionInterface + * @throws ParseException + */ + protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array { + $client = $this->clientService->newClient(); + $uri = $this->prepareUri($url, $addressBookUrl); + + $options = [ + 'auth' => [$userName, $sharedSecret], + 'body' => $this->buildSyncCollectionRequestBody($syncToken), + 'headers' => ['Content-Type' => 'application/xml'], + 'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT), + 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false), ]; - $client = new Client($settings); - $certPath = $this->getCertPath(); - $client->setThrowExceptions(true); - if ($certPath !== '' && !str_starts_with($url, 'http://')) { - $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath); - } + $response = $client->request( + 'REPORT', + $uri, + $options + ); - return $client; - } + $body = $response->getBody(); + assert(is_string($body)); - protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array { - $client = $this->getClient($url, $userName, $sharedSecret); + return $this->parseMultiStatus($body, $addressBookUrl); + } - $body = $this->buildSyncCollectionRequestBody($syncToken); + protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string { + $client = $this->clientService->newClient(); + $uri = $this->prepareUri($url, $resourcePath); - $response = $client->request('REPORT', $addressBookUrl, $body, [ - 'Content-Type' => 'application/xml' - ]); + $options = [ + 'auth' => [$userName, $sharedSecret], + 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false), + ]; - return $this->parseMultiStatus($response['body']); - } + $response = $client->get( + $uri, + $options + ); - protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): array { - $client = $this->getClient($url, $userName, $sharedSecret); - return $client->request('GET', $resourcePath); + return (string)$response->getBody(); } private function buildSyncCollectionRequestBody(?string $syncToken): string { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $root = $dom->createElementNS('DAV:', 'd:sync-collection'); - $sync = $dom->createElement('d:sync-token', $syncToken); + $sync = $dom->createElement('d:sync-token', $syncToken ?? ''); $prop = $dom->createElement('d:prop'); $cont = $dom->createElement('d:getcontenttype'); $etag = $dom->createElement('d:getetag'); @@ -198,22 +226,50 @@ class SyncService { } /** - * @param string $body - * @return array - * @throws \Sabre\Xml\ParseException + * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool} + * @throws ParseException */ - private function parseMultiStatus($body) { - $xml = new Service(); - + private function parseMultiStatus(string $body, string $addressBookUrl): array { /** @var MultiStatus $multiStatus */ - $multiStatus = $xml->expect('{DAV:}multistatus', $body); + $multiStatus = (new Service())->expect('{DAV:}multistatus', $body); $result = []; + $truncated = false; + foreach ($multiStatus->getResponses() as $response) { - $result[$response->getHref()] = $response->getResponseProperties(); + $href = $response->getHref(); + if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) { + $truncated = true; + } else { + $result[$response->getHref()] = $response->getResponseProperties(); + } } - return ['response' => $result, 'token' => $multiStatus->getSyncToken()]; + return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated]; + } + + /** + * Determines whether the provided response URI corresponds to the given request URI. + */ + private function isResponseForRequestUri(string $responseUri, string $requestUri): bool { + /* + * Example response uri: + * + * /remote.php/dav/addressbooks/system/system/system/ + * /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory) + * + * Example request uri: + * + * remote.php/dav/addressbooks/system/system/system + * + * References: + * https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174 + * https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41 + */ + return str_ends_with( + rtrim($responseUri, '/'), + rtrim($requestUri, '/') + ); } /** @@ -225,7 +281,7 @@ class SyncService { $cardId = self::getCardUri($user); if ($user->isEnabled()) { - $this->atomic(function () use ($addressBookId, $cardId, $user) { + $this->atomic(function () use ($addressBookId, $cardId, $user): void { $card = $this->backend->getCard($addressBookId, $cardId); if ($card === false) { $vCard = $this->converter->createCardFromUser($user); @@ -262,10 +318,7 @@ class SyncService { */ public function getLocalSystemAddressBook() { if (is_null($this->localSystemAddressBook)) { - $systemPrincipal = "principals/system/system"; - $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [ - '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance' - ]); + $this->localSystemAddressBook = $this->ensureLocalSystemAddressBookExists(); } return $this->localSystemAddressBook; @@ -276,7 +329,7 @@ class SyncService { */ public function syncInstance(?\Closure $progressCallback = null) { $systemAddressBook = $this->getLocalSystemAddressBook(); - $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) { + $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback): void { $this->updateUser($user); if (!is_null($progressCallback)) { $progressCallback(); diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index dc5ee0e1f21..912a2f1dcee 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -3,32 +3,11 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\CardDAV; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; @@ -50,27 +29,18 @@ use function in_array; class SystemAddressbook extends AddressBook { public const URI_SHARED = 'z-server-generated--system'; - /** @var IConfig */ - private $config; - private IUserSession $userSession; - private ?TrustedServers $trustedServers; - private ?IRequest $request; - private ?IGroupManager $groupManager; - public function __construct(BackendInterface $carddavBackend, + public function __construct( + BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, - IConfig $config, - IUserSession $userSession, - ?IRequest $request = null, - ?TrustedServers $trustedServers = null, - ?IGroupManager $groupManager = null) { + private IConfig $config, + private IUserSession $userSession, + private ?IRequest $request = null, + private ?TrustedServers $trustedServers = null, + private ?IGroupManager $groupManager = null, + ) { parent::__construct($carddavBackend, $addressBookInfo, $l10n); - $this->config = $config; - $this->userSession = $userSession; - $this->request = $request; - $this->trustedServers = $trustedServers; - $this->groupManager = $groupManager; $this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts'); $this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts'); @@ -241,14 +211,7 @@ class SystemAddressbook extends AddressBook { } return new Card($this->carddavBackend, $this->addressBookInfo, $obj); } - - /** - * @throws UnsupportedLimitOnInitialSyncException - */ public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } if (!$this->carddavBackend instanceof SyncSupport) { return null; @@ -274,7 +237,7 @@ class SystemAddressbook extends AddressBook { try { $this->getChild($uri); $added[] = $uri; - } catch (NotFound | Forbidden $e) { + } catch (NotFound|Forbidden $e) { $deleted[] = $uri; } } @@ -282,7 +245,7 @@ class SystemAddressbook extends AddressBook { try { $this->getChild($uri); $modified[] = $uri; - } catch (NotFound | Forbidden $e) { + } catch (NotFound|Forbidden $e) { $deleted[] = $uri; } } diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php index 2d129410067..e29e52e77df 100644 --- a/apps/dav/lib/CardDAV/UserAddressBooks.php +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -3,28 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @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\CardDAV; @@ -39,6 +20,7 @@ use OCP\IL10N; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use OCP\Server; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Sabre\CardDAV\Backend; @@ -54,20 +36,14 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { /** @var IConfig */ protected $config; - /** @var PluginManager */ - private $pluginManager; - private ?IUser $user; - private ?IGroupManager $groupManager; - - public function __construct(Backend\BackendInterface $carddavBackend, + public function __construct( + Backend\BackendInterface $carddavBackend, string $principalUri, - PluginManager $pluginManager, - ?IUser $user, - ?IGroupManager $groupManager) { + private PluginManager $pluginManager, + private ?IUser $user, + private ?IGroupManager $groupManager, + ) { parent::__construct($carddavBackend, $principalUri); - $this->pluginManager = $pluginManager; - $this->user = $user; - $this->groupManager = $groupManager; } /** @@ -80,7 +56,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { $this->l10n = \OC::$server->getL10N('dav'); } if ($this->config === null) { - $this->config = \OC::$server->getConfig(); + $this->config = Server::get(IConfig::class); } /** @var string|array $principal */ @@ -106,9 +82,9 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { $trustedServers = null; $request = null; try { - $trustedServers = \OC::$server->get(TrustedServers::class); - $request = \OC::$server->get(IRequest::class); - } catch (QueryException | NotFoundExceptionInterface | ContainerExceptionInterface $e) { + $trustedServers = Server::get(TrustedServers::class); + $request = Server::get(IRequest::class); + } catch (QueryException|NotFoundExceptionInterface|ContainerExceptionInterface $e) { // nothing to do, the request / trusted servers don't exist } if ($addressBook['principaluri'] === 'principals/system/system') { @@ -117,7 +93,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { $addressBook, $this->l10n, $this->config, - \OCP\Server::get(IUserSession::class), + Server::get(IUserSession::class), $request, $trustedServers, $this->groupManager diff --git a/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php new file mode 100644 index 00000000000..a5fd80ec124 --- /dev/null +++ b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\CardDAV\Validation; + +use OCA\DAV\AppInfo\Application; +use OCP\IAppConfig; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class CardDavValidatePlugin extends ServerPlugin { + + public function __construct( + private IAppConfig $config, + ) { + } + + public function initialize(Server $server): void { + $server->on('beforeMethod:PUT', [$this, 'beforePut']); + } + + public function beforePut(RequestInterface $request, ResponseInterface $response): bool { + // evaluate if card size exceeds defined limit + $cardSizeLimit = $this->config->getValueInt(Application::APP_ID, 'card_size_limit', 5242880); + if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $cardSizeLimit) { + throw new Forbidden("VCard object exceeds $cardSizeLimit bytes"); + } + // all tests passed return true + return true; + } + +} diff --git a/apps/dav/lib/CardDAV/Xml/Groups.php b/apps/dav/lib/CardDAV/Xml/Groups.php index bde36129382..07aeecb3fa2 100644 --- a/apps/dav/lib/CardDAV/Xml/Groups.php +++ b/apps/dav/lib/CardDAV/Xml/Groups.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\CardDAV\Xml; @@ -29,14 +13,12 @@ use Sabre\Xml\XmlSerializable; class Groups implements XmlSerializable { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; - /** @var string[] of TYPE:CHECKSUM */ - private $groups; - /** - * @param string $groups + * @param list<string> $groups */ - public function __construct($groups) { - $this->groups = $groups; + public function __construct( + private array $groups, + ) { } public function xmlSerialize(Writer $writer) { 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 27ecb5973e4..9626edeba26 100644 --- a/apps/dav/lib/Command/CreateAddressBook.php +++ b/apps/dav/lib/Command/CreateAddressBook.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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; @@ -41,14 +24,14 @@ class CreateAddressBook extends Command { protected function configure(): void { $this - ->setName('dav:create-addressbook') - ->setDescription('Create a dav addressbook') - ->addArgument('user', - InputArgument::REQUIRED, - 'User for whom the addressbook will be created') - ->addArgument('name', - InputArgument::REQUIRED, - 'Name of the addressbook'); + ->setName('dav:create-addressbook') + ->setDescription('Create a dav addressbook') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the addressbook will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the addressbook'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index 9acc1e147f8..033b5f8d347 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Citharel <nextcloud@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; @@ -31,11 +13,15 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Server; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -71,19 +57,19 @@ class CreateCalendar extends Command { $principalBackend = new Principal( $this->userManager, $this->groupManager, - \OC::$server->get(IAccountManager::class), - \OC::$server->getShareManager(), - \OC::$server->getUserSession(), - \OC::$server->getAppManager(), - \OC::$server->query(ProxyMapper::class), - \OC::$server->get(KnownUserService::class), - \OC::$server->getConfig(), + Server::get(IAccountManager::class), + Server::get(\OCP\Share\IManager::class), + Server::get(IUserSession::class), + Server::get(IAppManager::class), + Server::get(ProxyMapper::class), + Server::get(KnownUserService::class), + Server::get(IConfig::class), \OC::$server->getL10NFactory(), ); - $random = \OC::$server->getSecureRandom(); - $logger = \OC::$server->get(LoggerInterface::class); - $dispatcher = \OC::$server->get(IEventDispatcher::class); - $config = \OC::$server->get(IConfig::class); + $random = Server::get(ISecureRandom::class); + $logger = Server::get(LoggerInterface::class); + $dispatcher = Server::get(IEventDispatcher::class); + $config = Server::get(IConfig::class); $name = $input->getArgument('name'); $caldav = new CalDavBackend( $this->dbConnection, @@ -93,7 +79,7 @@ class CreateCalendar extends Command { $logger, $dispatcher, $config, - \OC::$server->get(Backend::class), + Server::get(Backend::class), ); $caldav->createCalendar("principals/users/$user", $name, []); return self::SUCCESS; diff --git a/apps/dav/lib/Command/CreateSubscription.php b/apps/dav/lib/Command/CreateSubscription.php new file mode 100644 index 00000000000..1364070e530 --- /dev/null +++ b/apps/dav/lib/Command/CreateSubscription.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\Theming\ThemingDefaults; +use OCP\IUserManager; +use Sabre\DAV\Xml\Property\Href; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateSubscription extends Command { + public function __construct( + protected IUserManager $userManager, + private CalDavBackend $caldav, + private ThemingDefaults $themingDefaults, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:create-subscription') + ->setDescription('Create a dav subscription') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the subscription will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the subscription to create') + ->addArgument('url', + InputArgument::REQUIRED, + 'Source url of the subscription to create') + ->addArgument('color', + InputArgument::OPTIONAL, + 'Hex color code for the calendar color'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + $output->writeln("<error>User <$user> in unknown.</error>"); + return self::FAILURE; + } + + $name = $input->getArgument('name'); + $url = $input->getArgument('url'); + $color = $input->getArgument('color') ?? $this->themingDefaults->getColorPrimary(); + $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user"); + + $exists = array_filter($subscriptions, function ($row) use ($url) { + return $row['source'] === $url; + }); + + if (!empty($exists)) { + $output->writeln("<error>Subscription for url <$url> already exists for this user.</error>"); + return self::FAILURE; + } + + $urlProperty = new Href($url); + $properties = ['{http://owncloud.org/ns}calendar-enabled' => 1, + '{DAV:}displayname' => $name, + '{http://apple.com/ns/ical/}calendar-color' => $color, + '{http://calendarserver.org/ns/}source' => $urlProperty, + ]; + $this->caldav->createSubscription("principals/users/$user", $name, $properties); + return self::SUCCESS; + } + +} diff --git a/apps/dav/lib/Command/DeleteCalendar.php b/apps/dav/lib/Command/DeleteCalendar.php index f9caa142757..f6dbed856e6 100644 --- a/apps/dav/lib/Command/DeleteCalendar.php +++ b/apps/dav/lib/Command/DeleteCalendar.php @@ -2,24 +2,8 @@ declare(strict_types=1); /** - * - * @copyright Copyright (c) 2021, Mattia Narducci (mattianarducci1@gmail.com) - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; @@ -70,9 +54,9 @@ class DeleteCalendar extends Command { protected function execute( InputInterface $input, - OutputInterface $output + OutputInterface $output, ): int { - /** @var string $user **/ + /** @var string $user */ $user = $input->getArgument('uid'); if (!$this->userManager->userExists($user)) { throw new \InvalidArgumentException( @@ -83,7 +67,7 @@ class DeleteCalendar extends Command { if ($birthday !== false) { $name = BirthdayService::BIRTHDAY_CALENDAR_URI; } else { - /** @var string $name **/ + /** @var string $name */ $name = $input->getArgument('name'); if (!$name) { throw new \InvalidArgumentException( diff --git a/apps/dav/lib/Command/DeleteSubscription.php b/apps/dav/lib/Command/DeleteSubscription.php new file mode 100644 index 00000000000..db0cb6295c9 --- /dev/null +++ b/apps/dav/lib/Command/DeleteSubscription.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CachedSubscription; +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:delete-subscription', + description: 'Delete a calendar subscription for a user', + hidden: false, +)] +class DeleteSubscription extends Command { + public function __construct( + private CalDavBackend $calDavBackend, + private IUserManager $userManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User who owns the calendar subscription' + ) + ->addArgument( + 'uri', + InputArgument::REQUIRED, + 'URI of the calendar to be deleted' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $uri = (string)$input->getArgument('uri'); + if ($uri === '') { + throw new \InvalidArgumentException('Specify the URI of the calendar to be deleted'); + } + + $subscriptionInfo = $this->calDavBackend->getSubscriptionByUri( + 'principals/users/' . $user, + $uri + ); + + if ($subscriptionInfo === null) { + throw new \InvalidArgumentException("User $user has no calendar subscription with the URI $uri"); + } + + $subscription = new CachedSubscription( + $this->calDavBackend, + $subscriptionInfo, + ); + + $subscription->delete(); + + $output->writeln("Calendar subscription with the URI $uri for user $user deleted"); + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php new file mode 100644 index 00000000000..6ed8aa2cfe4 --- /dev/null +++ b/apps/dav/lib/Command/ExportCalendar.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use InvalidArgumentException; +use OCA\DAV\CalDAV\Export\ExportService; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\ICalendarExport; +use OCP\Calendar\IManager; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Calendar Export Command + * + * Used to export data from supported calendars to disk or stdout + */ +#[AsCommand( + name: 'calendar:export', + description: 'Export calendar data from supported calendars to disk or stdout', + hidden: false +)] +class ExportCalendar extends Command { + public function __construct( + private IUserManager $userManager, + private IManager $calendarManager, + private ExportService $exportService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('calendar:export') + ->setDescription('Export calendar data from supported calendars to disk or stdout') + ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user') + ->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical') + ->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the output. defaults to stdout'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('uid'); + $calendarId = $input->getArgument('uri'); + $format = $input->getOption('format'); + $location = $input->getOption('location'); + + if (!$this->userManager->userExists($userId)) { + throw new InvalidArgumentException("User <$userId> not found."); + } + // retrieve calendar and evaluate if export is supported + $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]); + if ($calendars === []) { + throw new InvalidArgumentException("Calendar <$calendarId> not found."); + } + $calendar = $calendars[0]; + if (!$calendar instanceof ICalendarExport) { + throw new InvalidArgumentException("Calendar <$calendarId> does not support exporting"); + } + // construct options object + $options = new CalendarExportOptions(); + // evaluate if provided format is supported + if (!in_array($format, ExportService::FORMATS, true)) { + throw new InvalidArgumentException("Format <$format> is not valid."); + } + $options->setFormat($format); + // evaluate is a valid location was given and is usable otherwise output to stdout + if ($location !== null) { + $handle = fopen($location, 'wb'); + if ($handle === false) { + throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation."); + } + + foreach ($this->exportService->export($calendar, $options) as $chunk) { + fwrite($handle, $chunk); + } + fclose($handle); + } else { + foreach ($this->exportService->export($calendar, $options) as $chunk) { + $output->writeln($chunk); + } + } + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/FixCalendarSyncCommand.php b/apps/dav/lib/Command/FixCalendarSyncCommand.php index 6db32ff6d5e..cb31355c10d 100644 --- a/apps/dav/lib/Command/FixCalendarSyncCommand.php +++ b/apps/dav/lib/Command/FixCalendarSyncCommand.php @@ -2,25 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2024 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2024 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; @@ -36,8 +20,10 @@ use Symfony\Component\Console\Output\OutputInterface; class FixCalendarSyncCommand extends Command { - public function __construct(private IUserManager $userManager, - private CalDavBackend $calDavBackend) { + public function __construct( + private IUserManager $userManager, + private CalDavBackend $calDavBackend, + ) { parent::__construct('dav:fix-missing-caldav-changes'); } @@ -57,22 +43,23 @@ class FixCalendarSyncCommand extends Command { $user = $this->userManager->get($userArg); if ($user === null) { $output->writeln("<error>User $userArg does not exist</error>"); - return 1; + return self::FAILURE; } $this->fixUserCalendars($user); } else { $progress = new ProgressBar($output); - $this->userManager->callForSeenUsers(function (IUser $user) use ($progress) { + $this->userManager->callForSeenUsers(function (IUser $user) use ($progress): void { $this->fixUserCalendars($user, $progress); }); $progress->finish(); } - return 0; + $output->writeln(''); + return self::SUCCESS; } private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void { - $calendars = $this->calDavBackend->getCalendarsForUser("principals/users/" . $user->getUID()); + $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $user->getUID()); foreach ($calendars as $calendar) { $this->calDavBackend->restoreChanges($calendar['id']); diff --git a/apps/dav/lib/Command/GetAbsenceCommand.php b/apps/dav/lib/Command/GetAbsenceCommand.php new file mode 100644 index 00000000000..50d8df4ab38 --- /dev/null +++ b/apps/dav/lib/Command/GetAbsenceCommand.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\Service\AbsenceService; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GetAbsenceCommand extends Command { + + public function __construct( + private IUserManager $userManager, + private AbsenceService $absenceService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('dav:absence:get'); + $this->addArgument( + 'user-id', + InputArgument::REQUIRED, + 'User ID of the affected account' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('user-id'); + + $user = $this->userManager->get($userId); + if ($user === null) { + $output->writeln('<error>User not found</error>'); + return 1; + } + + $absence = $this->absenceService->getAbsence($userId); + if ($absence === null) { + $output->writeln('<info>No absence set</info>'); + return 0; + } + + $output->writeln('<info>Start day:</info> ' . $absence->getFirstDay()); + $output->writeln('<info>End day:</info> ' . $absence->getLastDay()); + $output->writeln('<info>Short message:</info> ' . $absence->getStatus()); + $output->writeln('<info>Message:</info> ' . $absence->getMessage()); + $output->writeln('<info>Replacement user:</info> ' . ($absence->getReplacementUserId() ?? 'none')); + $output->writeln('<info>Replacement display name:</info> ' . ($absence->getReplacementUserDisplayName() ?? 'none')); + + return 0; + } + +} diff --git a/apps/dav/lib/Command/ListAddressbooks.php b/apps/dav/lib/Command/ListAddressbooks.php new file mode 100644 index 00000000000..c0b6e63ccb8 --- /dev/null +++ b/apps/dav/lib/Command/ListAddressbooks.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\SystemAddressbook; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ListAddressbooks extends Command { + public function __construct( + protected IUserManager $userManager, + private CardDavBackend $cardDavBackend, + ) { + parent::__construct('dav:list-addressbooks'); + } + + protected function configure(): void { + $this + ->setDescription('List all addressbooks of a user') + ->addArgument('uid', + InputArgument::REQUIRED, + 'User for whom all addressbooks will be listed'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> is unknown."); + } + + $addressBooks = $this->cardDavBackend->getAddressBooksForUser("principals/users/$user"); + + $addressBookTableData = []; + foreach ($addressBooks as $book) { + // skip system / contacts integration address book + if ($book['uri'] === SystemAddressbook::URI_SHARED) { + continue; + } + + $readOnly = false; + $readOnlyIndex = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; + if (isset($book[$readOnlyIndex])) { + $readOnly = $book[$readOnlyIndex]; + } + + $addressBookTableData[] = [ + $book['uri'], + $book['{DAV:}displayname'], + $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] ?? $book['principaluri'], + $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'], + $readOnly ? ' x ' : ' ✓ ', + ]; + } + + if (count($addressBookTableData) > 0) { + $table = new Table($output); + $table->setHeaders(['Database ID', 'URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable']) + ->setRows($addressBookTableData); + + $table->render(); + } else { + $output->writeln("<info>User <$user> has no addressbooks</info>"); + } + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/ListCalendarShares.php b/apps/dav/lib/Command/ListCalendarShares.php new file mode 100644 index 00000000000..2729bc80530 --- /dev/null +++ b/apps/dav/lib/Command/ListCalendarShares.php @@ -0,0 +1,131 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Sharing\Backend; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:list-calendar-shares', + description: 'List all calendar shares for a user', + hidden: false, +)] +class ListCalendarShares extends Command { + public function __construct( + private IUserManager $userManager, + private Principal $principal, + private CalDavBackend $caldav, + private SharingMapper $mapper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User whose calendar shares will be listed' + ); + $this->addOption( + 'calendar-id', + '', + InputOption::VALUE_REQUIRED, + 'List only shares for the given calendar id id', + null, + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $principal = $this->principal->getPrincipalByPath('principals/users/' . $user); + if ($principal === null) { + throw new \InvalidArgumentException("Unable to fetch principal for user $user"); + } + + $memberships = array_merge( + [$principal['uri']], + $this->principal->getGroupMembership($principal['uri']), + $this->principal->getCircleMembership($principal['uri']), + ); + + $shares = $this->mapper->getSharesByPrincipals($memberships, 'calendar'); + + $calendarId = $input->getOption('calendar-id'); + if ($calendarId !== null) { + $shares = array_filter($shares, fn ($share) => $share['resourceid'] === (int)$calendarId); + } + + $rows = array_map(fn ($share) => $this->formatCalendarShare($share), $shares); + + if (count($rows) > 0) { + $table = new Table($output); + $table + ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name', 'Calendar Owner', 'Access By', 'Permissions']) + ->setRows($rows) + ->render(); + } else { + $output->writeln("User $user has no calendar shares"); + } + + return self::SUCCESS; + } + + private function formatCalendarShare(array $share): array { + $calendarInfo = $this->caldav->getCalendarById($share['resourceid']); + + $calendarUri = 'Resource not found'; + $calendarName = ''; + $calendarOwner = ''; + + if ($calendarInfo !== null) { + $calendarUri = $calendarInfo['uri']; + $calendarName = $calendarInfo['{DAV:}displayname']; + $calendarOwner = $calendarInfo['{http://nextcloud.com/ns}owner-displayname'] . ' (' . $calendarInfo['principaluri'] . ')'; + } + + $accessBy = match (true) { + str_starts_with($share['principaluri'], 'principals/users/') => 'Individual', + str_starts_with($share['principaluri'], 'principals/groups/') => 'Group (' . $share['principaluri'] . ')', + str_starts_with($share['principaluri'], 'principals/circles/') => 'Team (' . $share['principaluri'] . ')', + default => $share['principaluri'], + }; + + $permissions = match ($share['access']) { + Backend::ACCESS_READ => 'Read', + Backend::ACCESS_READ_WRITE => 'Read/Write', + Backend::ACCESS_UNSHARED => 'Unshare', + default => $share['access'], + }; + + return [ + $share['id'], + $share['resourceid'], + $calendarUri, + $calendarName, + $calendarOwner, + $accessBy, + $permissions, + ]; + } +} diff --git a/apps/dav/lib/Command/ListCalendars.php b/apps/dav/lib/Command/ListCalendars.php index 901d2822656..408a7e5247f 100644 --- a/apps/dav/lib/Command/ListCalendars.php +++ b/apps/dav/lib/Command/ListCalendars.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; @@ -83,7 +64,7 @@ class ListCalendars extends Command { if (count($calendarTableData) > 0) { $table = new Table($output); - $table->setHeaders(['uri', 'displayname', 'owner\'s userid', 'owner\'s displayname', 'writable']) + $table->setHeaders(['URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable']) ->setRows($calendarTableData); $table->render(); diff --git a/apps/dav/lib/Command/ListSubscriptions.php b/apps/dav/lib/Command/ListSubscriptions.php new file mode 100644 index 00000000000..67753f25973 --- /dev/null +++ b/apps/dav/lib/Command/ListSubscriptions.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IAppConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:list-subscriptions', + description: 'List all calendar subscriptions for a user', + hidden: false, +)] +class ListSubscriptions extends Command { + public function __construct( + private IUserManager $userManager, + private IAppConfig $appConfig, + private CalDavBackend $caldav, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User whose calendar subscriptions will be listed' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $defaultRefreshRate = $this->appConfig->getValueString('dav', 'calendarSubscriptionRefreshRate', 'P1D'); + $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user"); + $rows = []; + + foreach ($subscriptions as $subscription) { + $rows[] = [ + $subscription['uri'], + $subscription['{DAV:}displayname'], + $subscription['{http://apple.com/ns/ical/}refreshrate'] ?? ($defaultRefreshRate . ' (default)'), + $subscription['source'], + ]; + } + + usort($rows, static fn (array $a, array $b) => $a[0] <=> $b[0]); + + if (count($rows) > 0) { + $table = new Table($output); + $table + ->setHeaders(['URI', 'Displayname', 'Refresh rate', 'Source']) + ->setRows($rows) + ->render(); + } else { + $output->writeln("User $user has no subscriptions"); + } + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php index 7269a5ad4f3..b8acc191cc3 100644 --- a/apps/dav/lib/Command/MoveCalendar.php +++ b/apps/dav/lib/Command/MoveCalendar.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; @@ -71,7 +51,7 @@ class MoveCalendar extends Command { ->addArgument('destinationuid', InputArgument::REQUIRED, 'User who will receive the calendar') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts"); + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the migration by removing existing shares and renaming calendars in case of conflicts'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -118,8 +98,8 @@ class MoveCalendar extends Command { * Warn that share links have changed if there are shares */ $this->io->note([ - "Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.", - "Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\"" + 'Please note that moving calendar ' . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.", + 'Sharees will need to change "example.com/remote.php/dav/calendars/uid/' . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\"" ]); } @@ -176,7 +156,7 @@ class MoveCalendar extends Command { if ($force) { $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/groups/' . $userOrGroup]); } else { - throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share."); + throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . '> was shared. You may use -f to move the calendar while deleting this share.'); } } @@ -187,7 +167,7 @@ class MoveCalendar extends Command { if ($force) { $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/users/' . $userOrGroup]); } else { - throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share."); + throw new \InvalidArgumentException('The calendar <' . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share."); } } } diff --git a/apps/dav/lib/Command/RemoveInvalidShares.php b/apps/dav/lib/Command/RemoveInvalidShares.php index e373dc1c678..340e878a912 100644 --- a/apps/dav/lib/Command/RemoveInvalidShares.php +++ b/apps/dav/lib/Command/RemoveInvalidShares.php @@ -2,27 +2,11 @@ declare(strict_types=1); + /** - * @copyright Copyright (c) 2018, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2018 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Command; @@ -54,7 +38,7 @@ class RemoveInvalidShares extends Command { $query = $this->connection->getQueryBuilder(); $result = $query->selectDistinct('principaluri') ->from('dav_shares') - ->execute(); + ->executeQuery(); while ($row = $result->fetch()) { $principaluri = $row['principaluri']; @@ -75,6 +59,6 @@ class RemoveInvalidShares extends Command { $delete = $this->connection->getQueryBuilder(); $delete->delete('dav_shares') ->where($delete->expr()->eq('principaluri', $delete->createNamedParameter($principaluri))); - $delete->execute(); + $delete->executeStatement(); } } diff --git a/apps/dav/lib/Command/RetentionCleanupCommand.php b/apps/dav/lib/Command/RetentionCleanupCommand.php index 21eb96c235c..f1c941af20e 100644 --- a/apps/dav/lib/Command/RetentionCleanupCommand.php +++ b/apps/dav/lib/Command/RetentionCleanupCommand.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php index 16e3bb65a46..89bb5ce8c20 100644 --- a/apps/dav/lib/Command/SendEventReminders.php +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2019, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Command; 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 977fd5a067c..db1ebb6ecb5 100644 --- a/apps/dav/lib/Command/SyncBirthdayCalendar.php +++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @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; @@ -72,10 +55,10 @@ class SyncBirthdayCalendar extends Command { $this->birthdayService->syncUser($user); return self::SUCCESS; } - $output->writeln("Start birthday calendar sync for all users ..."); + $output->writeln('Start birthday calendar sync for all users ...'); $p = new ProgressBar($output); $p->start(); - $this->userManager->callForSeenUsers(function ($user) use ($p) { + $this->userManager->callForSeenUsers(function ($user) use ($p): void { $p->advance(); $userId = $user->getUID(); diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php index b05e65249ff..54edba01e05 100644 --- a/apps/dav/lib/Command/SyncSystemAddressBook.php +++ b/apps/dav/lib/Command/SyncSystemAddressBook.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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; @@ -48,7 +32,7 @@ class SyncSystemAddressBook extends Command { $output->writeln('Syncing users ...'); $progress = new ProgressBar($output); $progress->start(); - $this->syncService->syncInstance(function () use ($progress) { + $this->syncService->syncInstance(function () use ($progress): void { $progress->advance(); }); diff --git a/apps/dav/lib/Comments/CommentNode.php b/apps/dav/lib/Comments/CommentNode.php index 1d43c9c9309..5dbefa82d93 100644 --- a/apps/dav/lib/Comments/CommentNode.php +++ b/apps/dav/lib/Comments/CommentNode.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Comments; @@ -46,37 +30,19 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { public const PROPERTY_NAME_MENTION_ID = '{http://owncloud.org/ns}mentionId'; public const PROPERTY_NAME_MENTION_DISPLAYNAME = '{http://owncloud.org/ns}mentionDisplayName'; - /** @var IComment */ - public $comment; - - /** @var ICommentsManager */ - protected $commentsManager; - - protected LoggerInterface $logger; - /** @var array list of properties with key being their name and value their setter */ protected $properties = []; - /** @var IUserManager */ - protected $userManager; - - /** @var IUserSession */ - protected $userSession; - /** * CommentNode constructor. */ public function __construct( - ICommentsManager $commentsManager, - IComment $comment, - IUserManager $userManager, - IUserSession $userSession, - LoggerInterface $logger + protected ICommentsManager $commentsManager, + public IComment $comment, + protected IUserManager $userManager, + protected IUserSession $userSession, + protected LoggerInterface $logger, ) { - $this->commentsManager = $commentsManager; - $this->comment = $comment; - $this->logger = $logger; - $methods = get_class_methods($this->comment); $methods = array_filter($methods, function ($name) { return str_starts_with($name, 'get'); @@ -85,11 +51,9 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { if ($getter === 'getMentions') { continue; // special treatment } - $name = '{'.self::NS_OWNCLOUD.'}' . lcfirst(substr($getter, 3)); + $name = '{' . self::NS_OWNCLOUD . '}' . lcfirst(substr($getter, 3)); $this->properties[$name] = $getter; } - $this->userManager = $userManager; - $this->userSession = $userSession; } /** diff --git a/apps/dav/lib/Comments/CommentsPlugin.php b/apps/dav/lib/Comments/CommentsPlugin.php index 1cfaa8b4e16..2ab7d6ee018 100644 --- a/apps/dav/lib/Comments/CommentsPlugin.php +++ b/apps/dav/lib/Comments/CommentsPlugin.php @@ -1,32 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Comments; +use OCP\AppFramework\Http; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; +use OCP\Comments\MessageTooLongException; use OCP\IUserSession; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\NotFound; @@ -52,24 +36,19 @@ class CommentsPlugin extends ServerPlugin { public const REPORT_PARAM_OFFSET = '{http://owncloud.org/ns}offset'; public const REPORT_PARAM_TIMESTAMP = '{http://owncloud.org/ns}datetime'; - /** @var ICommentsManager */ - protected $commentsManager; - /** @var \Sabre\DAV\Server $server */ private $server; - /** @var \OCP\IUserSession */ - protected $userSession; - /** * Comments plugin * * @param ICommentsManager $commentsManager * @param IUserSession $userSession */ - public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { - $this->commentsManager = $commentsManager; - $this->userSession = $userSession; + public function __construct( + protected ICommentsManager $commentsManager, + protected IUserSession $userSession, + ) { } /** @@ -91,7 +70,7 @@ class CommentsPlugin extends ServerPlugin { $this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; - $this->server->xml->classMap['DateTime'] = function (Writer $writer, \DateTime $value) { + $this->server->xml->classMap['DateTime'] = function (Writer $writer, \DateTime $value): void { $writer->write(\Sabre\HTTP\toDate($value)); }; @@ -130,7 +109,7 @@ class CommentsPlugin extends ServerPlugin { $response->setHeader('Content-Location', $url); // created - $response->setStatus(201); + $response->setStatus(Http::STATUS_CREATED); return false; } @@ -199,7 +178,7 @@ class CommentsPlugin extends ServerPlugin { new MultiStatus($responses) ); - $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setBody($xml); @@ -234,7 +213,7 @@ class CommentsPlugin extends ServerPlugin { } } if (is_null($actorId)) { - throw new BadRequest('Invalid actor "' . $actorType .'"'); + throw new BadRequest('Invalid actor "' . $actorType . '"'); } try { @@ -245,9 +224,9 @@ class CommentsPlugin extends ServerPlugin { return $comment; } catch (\InvalidArgumentException $e) { throw new BadRequest('Invalid input values', 0, $e); - } catch (\OCP\Comments\MessageTooLongException $e) { + } catch (MessageTooLongException $e) { $msg = 'Message exceeds allowed character limit of '; - throw new BadRequest($msg . \OCP\Comments\IComment::MAX_MESSAGE_LENGTH, 0, $e); + throw new BadRequest($msg . IComment::MAX_MESSAGE_LENGTH, 0, $e); } } } diff --git a/apps/dav/lib/Comments/EntityCollection.php b/apps/dav/lib/Comments/EntityCollection.php index 1b6139a34a2..33c58ee44d2 100644 --- a/apps/dav/lib/Comments/EntityCollection.php +++ b/apps/dav/lib/Comments/EntityCollection.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @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\Comments; @@ -43,11 +27,6 @@ use Sabre\DAV\PropPatch; class EntityCollection extends RootCollection implements IProperties { public const PROPERTY_NAME_READ_MARKER = '{http://owncloud.org/ns}readMarker'; - /** @var string */ - protected $id; - - protected LoggerInterface $logger; - /** * @param string $id * @param string $name @@ -57,12 +36,12 @@ class EntityCollection extends RootCollection implements IProperties { * @param LoggerInterface $logger */ public function __construct( - $id, + protected $id, $name, ICommentsManager $commentsManager, IUserManager $userManager, IUserSession $userSession, - LoggerInterface $logger + protected LoggerInterface $logger, ) { foreach (['id', 'name'] as $property) { $$property = trim($$property); @@ -70,10 +49,8 @@ class EntityCollection extends RootCollection implements IProperties { throw new \InvalidArgumentException('"' . $property . '" parameter must be non-empty string'); } } - $this->id = $id; $this->name = $name; $this->commentsManager = $commentsManager; - $this->logger = $logger; $this->userManager = $userManager; $this->userSession = $userSession; } diff --git a/apps/dav/lib/Comments/EntityTypeCollection.php b/apps/dav/lib/Comments/EntityTypeCollection.php index 32adcee54f0..1c8533ca375 100644 --- a/apps/dav/lib/Comments/EntityTypeCollection.php +++ b/apps/dav/lib/Comments/EntityTypeCollection.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @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\Comments; @@ -42,19 +26,13 @@ use Sabre\DAV\Exception\NotFound; * @package OCA\DAV\Comments */ class EntityTypeCollection extends RootCollection { - protected LoggerInterface $logger; - protected IUserManager $userManager; - - /** @var \Closure */ - protected $childExistsFunction; - public function __construct( string $name, ICommentsManager $commentsManager, - IUserManager $userManager, + protected IUserManager $userManager, IUserSession $userSession, - LoggerInterface $logger, - \Closure $childExistsFunction + protected LoggerInterface $logger, + protected \Closure $childExistsFunction, ) { $name = trim($name); if (empty($name)) { @@ -62,10 +40,7 @@ class EntityTypeCollection extends RootCollection { } $this->name = $name; $this->commentsManager = $commentsManager; - $this->logger = $logger; - $this->userManager = $userManager; $this->userSession = $userSession; - $this->childExistsFunction = $childExistsFunction; } /** diff --git a/apps/dav/lib/Comments/RootCollection.php b/apps/dav/lib/Comments/RootCollection.php index 956980c900d..493d73ec531 100644 --- a/apps/dav/lib/Comments/RootCollection.php +++ b/apps/dav/lib/Comments/RootCollection.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\Comments; @@ -38,24 +21,15 @@ use Sabre\DAV\ICollection; class RootCollection implements ICollection { /** @var EntityTypeCollection[]|null */ private ?array $entityTypeCollections = null; - protected ICommentsManager $commentsManager; protected string $name = 'comments'; - protected LoggerInterface $logger; - protected IUserManager $userManager; - protected IUserSession $userSession; - protected IEventDispatcher $dispatcher; public function __construct( - ICommentsManager $commentsManager, - IUserManager $userManager, - IUserSession $userSession, - IEventDispatcher $dispatcher, - LoggerInterface $logger) { - $this->commentsManager = $commentsManager; - $this->logger = $logger; - $this->userManager = $userManager; - $this->userSession = $userSession; - $this->dispatcher = $dispatcher; + protected ICommentsManager $commentsManager, + protected IUserManager $userManager, + protected IUserSession $userSession, + protected IEventDispatcher $dispatcher, + protected LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/Connector/LegacyDAVACL.php b/apps/dav/lib/Connector/LegacyDAVACL.php index da570b235de..40ce53b8ab0 100644 --- a/apps/dav/lib/Connector/LegacyDAVACL.php +++ b/apps/dav/lib/Connector/LegacyDAVACL.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Connector; diff --git a/apps/dav/lib/Connector/LegacyPublicAuth.php b/apps/dav/lib/Connector/LegacyPublicAuth.php index 82a474d4bed..03d18853de0 100644 --- a/apps/dav/lib/Connector/LegacyPublicAuth.php +++ b/apps/dav/lib/Connector/LegacyPublicAuth.php @@ -1,35 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector; use OCA\DAV\Connector\Sabre\PublicAuth; +use OCP\Defaults; use OCP\IRequest; use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; @@ -47,22 +26,15 @@ class LegacyPublicAuth extends AbstractBasic { private const BRUTEFORCE_ACTION = 'legacy_public_webdav_auth'; private ?IShare $share = null; - private IManager $shareManager; - private ISession $session; - private IRequest $request; - private IThrottler $throttler; - - public function __construct(IRequest $request, - IManager $shareManager, - ISession $session, - IThrottler $throttler) { - $this->request = $request; - $this->shareManager = $shareManager; - $this->session = $session; - $this->throttler = $throttler; + public function __construct( + private IRequest $request, + private IManager $shareManager, + private ISession $session, + private IThrottler $throttler, + ) { // setup realm - $defaults = new \OCP\Defaults(); + $defaults = new Defaults(); $this->realm = $defaults->getName() ?: 'Nextcloud'; } diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php index d8be0c20dc8..0e2b1c58748 100644 --- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Bastien Durel <bastien@durel.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php index db0f4e56b2e..9cff113140a 100644 --- a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2023 Claus-Justus Heine - * - * @author Claus-Justus Heine <himself@claus-justus-heine.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; @@ -75,8 +59,8 @@ class AppleQuirksPlugin extends ServerPlugin { * This method handles HTTP REPORT requests. * * @param string $reportName - * @param mixed $report - * @param mixed $path + * @param mixed $report + * @param mixed $path * * @return bool */ diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php index c2e343d8656..a174920946a 100644 --- a/apps/dav/lib/Connector/Sabre/Auth.php +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -1,35 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Markus Goetz <markus@woboq.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -39,10 +13,13 @@ use OC\Authentication\TwoFactorAuth\Manager; use OC\User\Session; use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden; use OCA\DAV\Connector\Sabre\Exception\TooManyRequests; +use OCP\AppFramework\Http; +use OCP\Defaults; use OCP\IRequest; use OCP\ISession; use OCP\Security\Bruteforce\IThrottler; use OCP\Security\Bruteforce\MaxDelayReached; +use OCP\Server; use Psr\Log\LoggerInterface; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Exception\NotAuthenticated; @@ -52,29 +29,20 @@ use Sabre\HTTP\ResponseInterface; class Auth extends AbstractBasic { public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND'; - - private ISession $session; - private Session $userSession; - private IRequest $request; private ?string $currentUser = null; - private Manager $twoFactorManager; - private IThrottler $throttler; - - public function __construct(ISession $session, - Session $userSession, - IRequest $request, - Manager $twoFactorManager, - IThrottler $throttler, - string $principalPrefix = 'principals/users/') { - $this->session = $session; - $this->userSession = $userSession; - $this->twoFactorManager = $twoFactorManager; - $this->request = $request; - $this->throttler = $throttler; + + public function __construct( + private ISession $session, + private Session $userSession, + private IRequest $request, + private Manager $twoFactorManager, + private IThrottler $throttler, + string $principalPrefix = 'principals/users/', + ) { $this->principalPrefix = $principalPrefix; // setup realm - $defaults = new \OCP\Defaults(); + $defaults = new Defaults(); $this->realm = $defaults->getName() ?: 'Nextcloud'; } @@ -87,8 +55,8 @@ class Auth extends AbstractBasic { * @see https://github.com/owncloud/core/issues/13245 */ public function isDavAuthenticated(string $username): bool { - return !is_null($this->session->get(self::DAV_AUTHENTICATED)) && - $this->session->get(self::DAV_AUTHENTICATED) === $username; + return !is_null($this->session->get(self::DAV_AUTHENTICATED)) + && $this->session->get(self::DAV_AUTHENTICATED) === $username; } /** @@ -103,8 +71,8 @@ class Auth extends AbstractBasic { * @throws PasswordLoginForbidden */ protected function validateUserPass($username, $password) { - if ($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID()) + if ($this->userSession->isLoggedIn() + && $this->isDavAuthenticated($this->userSession->getUser()->getUID()) ) { $this->session->close(); return true; @@ -141,7 +109,7 @@ class Auth extends AbstractBasic { } catch (Exception $e) { $class = get_class($e); $msg = $e->getMessage(); - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); throw new ServiceUnavailable("$class: $msg"); } } @@ -150,8 +118,9 @@ class Auth extends AbstractBasic { * Checks whether a CSRF check is required on the request */ private function requiresCSRFCheck(): bool { - // GET requires no check at all - if ($this->request->getMethod() === 'GET') { + + $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS']; + if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) { return false; } @@ -175,8 +144,8 @@ class Auth extends AbstractBasic { } // If logged-in AND DAV authenticated no check is required - if ($this->userSession->isLoggedIn() && - $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { + if ($this->userSession->isLoggedIn() + && $this->isDavAuthenticated($this->userSession->getUser()->getUID())) { return false; } @@ -190,13 +159,13 @@ class Auth extends AbstractBasic { private function auth(RequestInterface $request, ResponseInterface $response): array { $forcedLogout = false; - if (!$this->request->passesCSRFCheck() && - $this->requiresCSRFCheck()) { + if (!$this->request->passesCSRFCheck() + && $this->requiresCSRFCheck()) { // In case of a fail with POST we need to recheck the credentials if ($this->request->getMethod() === 'POST') { $forcedLogout = true; } else { - $response->setStatus(401); + $response->setStatus(Http::STATUS_UNAUTHORIZED); throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.'); } } @@ -209,10 +178,10 @@ class Auth extends AbstractBasic { } if ( //Fix for broken webdav clients - ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) || + ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) //Well behaved clients that only send the cookie are allowed - ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) || - \OC_User::handleApacheAuth() + || ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization'))) + || \OC_User::handleApacheAuth() ) { $user = $this->userSession->getUser()->getUID(); $this->currentUser = $user; @@ -221,18 +190,16 @@ class Auth extends AbstractBasic { } } - if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) { - // do not re-authenticate over ajax, use dummy auth name to prevent browser popup - $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"'); - $response->setStatus(401); - throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); - } - $data = parent::check($request, $response); if ($data[0] === true) { $startPos = strrpos($data[1], '/') + 1; $user = $this->userSession->getUser()->getUID(); $data[1] = substr_replace($data[1], $user, $startPos); + } elseif (in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) { + // For ajax requests use dummy auth name to prevent browser popup in case of invalid creditials + $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"'); + $response->setStatus(Http::STATUS_UNAUTHORIZED); + throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls'); } return $data; } diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php index 6fd61c44b34..23453ae8efb 100644 --- a/apps/dav/lib/Connector/Sabre/BearerAuth.php +++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php @@ -1,28 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; +use OCP\AppFramework\Http; +use OCP\Defaults; +use OCP\IConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -31,22 +17,15 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class BearerAuth extends AbstractBearer { - private IUserSession $userSession; - private ISession $session; - private IRequest $request; - private string $principalPrefix; - - public function __construct(IUserSession $userSession, - ISession $session, - IRequest $request, - $principalPrefix = 'principals/users/') { - $this->userSession = $userSession; - $this->session = $session; - $this->request = $request; - $this->principalPrefix = $principalPrefix; - + public function __construct( + private IUserSession $userSession, + private ISession $session, + private IRequest $request, + private IConfig $config, + private string $principalPrefix = 'principals/users/', + ) { // setup realm - $defaults = new \OCP\Defaults(); + $defaults = new Defaults(); $this->realm = $defaults->getName() ?: 'Nextcloud'; } @@ -81,6 +60,14 @@ class BearerAuth extends AbstractBearer { * @param ResponseInterface $response */ public function challenge(RequestInterface $request, ResponseInterface $response): void { - $response->setStatus(401); + // Legacy ownCloud clients still authenticate via OAuth2 + $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false); + $userAgent = $request->getHeader('User-Agent'); + if ($enableOcClients && $userAgent !== null && str_contains($userAgent, 'mirall')) { + parent::challenge($request, $response); + return; + } + + $response->setStatus(Http::STATUS_UNAUTHORIZED); } } diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php index eda2399a780..21358406a4a 100644 --- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php +++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Connector\Sabre; +use OCA\Theming\ThemingDefaults; use OCP\IConfig; use OCP\IRequest; use Sabre\DAV\Server; @@ -39,10 +22,11 @@ use Sabre\HTTP\RequestInterface; */ class BlockLegacyClientPlugin extends ServerPlugin { protected ?Server $server = null; - protected IConfig $config; - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private IConfig $config, + private ThemingDefaults $themingDefaults, + ) { } /** @@ -65,11 +49,26 @@ class BlockLegacyClientPlugin extends ServerPlugin { return; } - $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.3.0'); + $minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '3.1.0'); + $maximumSupportedDesktopVersion = $this->config->getSystemValueString('maximum.supported.desktop.version', '99.99.99'); + + // Check if the client is a desktop client preg_match(IRequest::USER_AGENT_CLIENT_DESKTOP, $userAgent, $versionMatches); - if (isset($versionMatches[1]) && - version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) { - throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.'); + + // If the client is a desktop client and the version is too old, block it + if (isset($versionMatches[1]) && version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) { + $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl()); + $minimumSupportedDesktopVersion = htmlspecialchars($minimumSupportedDesktopVersion); + + throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Upgrade to <a href=\"$customClientDesktopLink\">version $minimumSupportedDesktopVersion or later</a>."); + } + + // If the client is a desktop client and the version is too new, block it + if (isset($versionMatches[1]) && version_compare($versionMatches[1], $maximumSupportedDesktopVersion) === 1) { + $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl()); + $maximumSupportedDesktopVersion = htmlspecialchars($maximumSupportedDesktopVersion); + + throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Downgrade to <a href=\"$customClientDesktopLink\">version $maximumSupportedDesktopVersion or earlier</a>."); } } } diff --git a/apps/dav/lib/Connector/Sabre/CachingTree.php b/apps/dav/lib/Connector/Sabre/CachingTree.php index 54038468dc5..5d72b530f58 100644 --- a/apps/dav/lib/Connector/Sabre/CachingTree.php +++ b/apps/dav/lib/Connector/Sabre/CachingTree.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; @@ -45,7 +28,7 @@ class CachingTree extends Tree { // flushing the entire cache $path = trim($path, '/'); foreach ($this->cache as $nodePath => $node) { - $nodePath = (string) $nodePath; + $nodePath = (string)$nodePath; if ($path === '' || $nodePath == $path || str_starts_with($nodePath, $path . '/')) { unset($this->cache[$nodePath]); } diff --git a/apps/dav/lib/Connector/Sabre/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php index 8e01dcc0f4b..75d1d718de1 100644 --- a/apps/dav/lib/Connector/Sabre/ChecksumList.php +++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\Connector\Sabre; diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php index d68ced83616..18009080585 100644 --- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php +++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php @@ -2,27 +2,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; +use OCP\AppFramework\Http; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -41,20 +27,6 @@ class ChecksumUpdatePlugin extends ServerPlugin { } /** @return string[] */ - public function getHTTPMethods($path): array { - $tree = $this->server->tree; - - if ($tree->nodeExists($path)) { - $node = $tree->getNodeForPath($path); - if ($node instanceof File) { - return ['PATCH']; - } - } - - return []; - } - - /** @return string[] */ public function getFeatures(): array { return ['nextcloud-checksum-update']; } @@ -74,7 +46,7 @@ class ChecksumUpdatePlugin extends ServerPlugin { $node->setChecksum($checksum); $response->addHeader('OC-Checksum', $checksum); $response->setHeader('Content-Length', '0'); - $response->setStatus(204); + $response->setStatus(Http::STATUS_NO_CONTENT); return false; } diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php index 94f9e167aae..e4b6c2636da 100644 --- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php @@ -2,28 +2,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\Connector\Sabre; @@ -39,13 +20,12 @@ class CommentPropertiesPlugin extends ServerPlugin { public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread'; protected ?Server $server = null; - private ICommentsManager $commentsManager; - private IUserSession $userSession; private array $cachedUnreadCount = []; - public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) { - $this->commentsManager = $commentsManager; - $this->userSession = $userSession; + public function __construct( + private ICommentsManager $commentsManager, + private IUserSession $userSession, + ) { } /** @@ -81,7 +61,7 @@ class CommentPropertiesPlugin extends ServerPlugin { $ids[] = (string)$id; } - $ids[] = (string) $directory->getId(); + $ids[] = (string)$directory->getId(); $unread = $this->commentsManager->getNumberOfUnreadCommentsForObjects('files', $ids, $this->userSession->getUser()); foreach ($unread as $id => $count) { @@ -99,7 +79,7 @@ class CommentPropertiesPlugin extends ServerPlugin { */ public function handleGetProperties( PropFind $propFind, - \Sabre\DAV\INode $node + \Sabre\DAV\INode $node, ) { if (!($node instanceof File) && !($node instanceof Directory)) { return; diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php index 0533dcab4d9..609ac295b4c 100644 --- a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php +++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php index 61fac7250bc..100d719ef01 100644 --- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -1,35 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @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\Connector\Sabre; +use OCA\DAV\CalDAV\CachedSubscription; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CardDAV\AddressBook; use Sabre\CalDAV\Principal\User; +use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; use Sabre\DAV\PropFind; @@ -61,19 +43,26 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin { $type = 'Addressbook'; break; case Calendar::class: + case CachedSubscription::class: $type = 'Calendar'; break; default: $type = 'Node'; break; } - throw new NotFound( - sprintf( - "%s with name '%s' could not be found", - $type, - $node->getName() - ) - ); + + if ($this->getCurrentUserPrincipal() === $node->getOwner()) { + throw new Forbidden('Access denied'); + } else { + throw new NotFound( + sprintf( + "%s with name '%s' could not be found", + $type, + $node->getName() + ) + ); + } + } return $access; diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index e40b8d760eb..fe09c3f423f 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -1,34 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -38,10 +13,15 @@ use OCA\DAV\AppInfo\Application; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Storage\PublicShareWrapper; +use OCP\App\IAppManager; +use OCP\Constants; use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\ForbiddenException; use OCP\Files\InvalidPathException; +use OCP\Files\Mount\IMountManager; +use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\StorageNotAvailableException; use OCP\IL10N; @@ -49,6 +29,7 @@ use OCP\IRequest; use OCP\L10N\IFactory; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; +use OCP\Server; use OCP\Share\IManager as IShareManager; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\BadRequest; @@ -58,23 +39,26 @@ use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\DAV\IFile; use Sabre\DAV\INode; -class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget { +class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget { /** * Cached directory content - * @var \OCP\Files\FileInfo[] + * @var FileInfo[] */ private ?array $dirContent = null; /** Cached quota info */ private ?array $quotaInfo = null; - private ?CachingTree $tree = null; /** * Sets up the node, expects a full path name */ - public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, ?IShareManager $shareManager = null) { + public function __construct( + View $view, + FileInfo $info, + private ?CachingTree $tree = null, + ?IShareManager $shareManager = null, + ) { parent::__construct($view, $info, $shareManager); - $this->tree = $tree; } /** @@ -111,21 +95,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol */ public function createFile($name, $data = null) { try { - // for chunked upload also updating a existing file is a "createFile" - // because we create all the chunks before re-assemble them to the existing file. - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - // exit if we can't create a new file and we don't updatable existing file - $chunkInfo = \OC_FileChunking::decodeName($name); - if (!$this->fileView->isCreatable($this->path) && - !$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name']) - ) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - } else { - // For non-chunked upload it is enough to check if we can create a new file - if (!$this->fileView->isCreatable($this->path)) { - throw new \Sabre\DAV\Exception\Forbidden(); - } + if (!$this->fileView->isCreatable($this->path)) { + throw new \Sabre\DAV\Exception\Forbidden(); } $this->fileView->verifyPath($this->path, $name); @@ -139,18 +110,18 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol 'type' => FileInfo::TYPE_FILE ], null); } - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); + $node = new File($this->fileView, $info); // only allow 1 process to upload a file at once but still allow reading the file while writing the part file $node->acquireLock(ILockingProvider::LOCK_SHARED); - $this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE); + $this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE); $result = $node->put($data); - $this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE); + $this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE); $node->releaseLock(ILockingProvider::LOCK_SHARED); return $result; - } catch (\OCP\Files\StorageNotAvailableException $e) { + } catch (StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e); } catch (InvalidPathException $ex) { throw new InvalidPath($ex->getMessage(), false, $ex); @@ -181,12 +152,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol if (!$this->fileView->mkdir($newPath)) { throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath); } - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e); } catch (InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); + throw new InvalidPath($ex->getMessage(), false, $ex); } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); + throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -196,14 +167,27 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol * Returns a specific child node, referenced by its name * * @param string $name - * @param \OCP\Files\FileInfo $info + * @param FileInfo $info * @return \Sabre\DAV\INode * @throws InvalidPath * @throws \Sabre\DAV\Exception\NotFound * @throws \Sabre\DAV\Exception\ServiceUnavailable */ public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) { - if (!$this->info->isReadable()) { + $storage = $this->info->getStorage(); + $allowDirectory = false; + + // Checking if we're in a file drop + // If we are, then only PUT and MKCOL are allowed (see plugin) + // so we are safe to return the directory without a risk of + // leaking files and folders structure. + if ($storage instanceof PublicShareWrapper) { + $share = $storage->getShare(); + $allowDirectory = ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ; + } + + // For file drop we need to be allowed to read the directory with the nickname + if (!$allowDirectory && !$this->info->isReadable()) { // avoid detecting files through this way throw new NotFound(); } @@ -211,14 +195,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol $path = $this->path . '/' . $name; if (is_null($info)) { try { - $this->fileView->verifyPath($this->path, $name); + $this->fileView->verifyPath($this->path, $name, true); $info = $this->fileView->getFileInfo($path); - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (StorageNotAvailableException $e) { + throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e); } catch (InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); + throw new InvalidPath($ex->getMessage(), false, $ex); } catch (ForbiddenException $e) { - throw new \Sabre\DAV\Exception\Forbidden(); + throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e); } } @@ -229,7 +213,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) { $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager); } else { - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n); + // In case reading a directory was allowed but it turns out the node was a not a directory, reject it now. + if (!$this->info->isReadable()) { + throw new NotFound(); + } + + $node = new File($this->fileView, $info, $this->shareManager, $request, $l10n); } if ($this->tree) { $this->tree->cacheNode($node); @@ -242,7 +231,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol * * @return \Sabre\DAV\INode[] * @throws \Sabre\DAV\Exception\Locked - * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden + * @throws Forbidden */ public function getChildren() { if (!is_null($this->dirContent)) { @@ -252,7 +241,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol if (!$this->info->isReadable()) { // return 403 instead of 404 because a 404 would make // the caller believe that the collection itself does not exist - if (\OCP\Server::get(\OCP\App\IAppManager::class)->isInstalled('files_accesscontrol')) { + if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) { throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules'); } else { throw new Forbidden('No read permissions'); @@ -264,8 +253,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol } $nodes = []; - $request = \OC::$server->get(IRequest::class); - $l10nFactory = \OC::$server->get(IFactory::class); + $request = Server::get(IRequest::class); + $l10nFactory = Server::get(IFactory::class); $l10n = $l10nFactory->get(Application::APP_ID); foreach ($folderContent as $info) { $node = $this->getChild($info->getName(), $info, $request, $l10n); @@ -318,7 +307,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol } private function getLogger(): LoggerInterface { - return \OC::$server->get(LoggerInterface::class); + return Server::get(LoggerInterface::class); } /** @@ -332,14 +321,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol } $relativePath = $this->fileView->getRelativePath($this->info->getPath()); if ($relativePath === null) { - $this->getLogger()->warning("error while getting quota as the relative path cannot be found"); + $this->getLogger()->warning('error while getting quota as the relative path cannot be found'); return [0, 0]; } try { $storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false); - if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) { - $free = \OCP\Files\FileInfo::SPACE_UNLIMITED; + if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) { + $free = FileInfo::SPACE_UNLIMITED; } else { $free = $storageInfo['free']; } @@ -348,14 +337,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol $free ]; return $this->quotaInfo; - } catch (\OCP\Files\NotFoundException $e) { - $this->getLogger()->warning("error while getting quota into", ['exception' => $e]); + } catch (NotFoundException $e) { + $this->getLogger()->warning('error while getting quota into', ['exception' => $e]); return [0, 0]; - } catch (\OCP\Files\StorageNotAvailableException $e) { - $this->getLogger()->warning("error while getting quota into", ['exception' => $e]); + } catch (StorageNotAvailableException $e) { + $this->getLogger()->warning('error while getting quota into', ['exception' => $e]); return [0, 0]; } catch (NotPermittedException $e) { - $this->getLogger()->warning("error while getting quota into", ['exception' => $e]); + $this->getLogger()->warning('error while getting quota into', ['exception' => $e]); return [0, 0]; } } @@ -395,10 +384,6 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol throw new BadRequest('Incompatible node types'); } - if (!$this->fileView) { - throw new ServiceUnavailable('filesystem not setup'); - } - $destinationPath = $this->getPath() . '/' . $targetName; @@ -416,7 +401,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol $sourcePath = $sourceNode->getPath(); $isMovableMount = false; - $sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath)); + $sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath)); $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath)); if ($sourceMount instanceof MoveableMount && $internalPath === '') { $isMovableMount = true; @@ -456,9 +441,9 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol throw new \Sabre\DAV\Exception\Forbidden(''); } } catch (StorageNotAvailableException $e) { - throw new ServiceUnavailable($e->getMessage()); + throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e); } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry()); + throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex); } catch (LockedException $e) { throw new FileLocked($e->getMessage(), $e->getCode(), $e); } @@ -469,20 +454,34 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol public function copyInto($targetName, $sourcePath, INode $sourceNode) { if ($sourceNode instanceof File || $sourceNode instanceof Directory) { - $destinationPath = $this->getPath() . '/' . $targetName; - $sourcePath = $sourceNode->getPath(); + try { + $destinationPath = $this->getPath() . '/' . $targetName; + $sourcePath = $sourceNode->getPath(); - if (!$this->fileView->isCreatable($this->getPath())) { - throw new \Sabre\DAV\Exception\Forbidden(); - } + if (!$this->fileView->isCreatable($this->getPath())) { + throw new \Sabre\DAV\Exception\Forbidden(); + } - try { - $this->fileView->verifyPath($this->getPath(), $targetName); - } catch (InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage()); - } + try { + $this->fileView->verifyPath($this->getPath(), $targetName); + } catch (InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage()); + } - return $this->fileView->copy($sourcePath, $destinationPath); + $copyOkay = $this->fileView->copy($sourcePath, $destinationPath); + + if (!$copyOkay) { + throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed'); + } + + return true; + } catch (StorageNotAvailableException $e) { + throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } } return false; diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php index 2019c77ad35..f6baceb748b 100644 --- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php +++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php @@ -1,31 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wickert <cwickert@suse.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Connector\Sabre; +use OCP\AppFramework\Http; use Sabre\DAV\Server; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; @@ -61,13 +43,13 @@ class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin { * @return false */ public function httpGet(RequestInterface $request, ResponseInterface $response) { - $string = 'This is the WebDAV interface. It can only be accessed by ' . - 'WebDAV clients such as the Nextcloud desktop sync client.'; + $string = 'This is the WebDAV interface. It can only be accessed by ' + . 'WebDAV clients such as the Nextcloud desktop sync client.'; $stream = fopen('php://memory', 'r+'); fwrite($stream, $string); rewind($stream); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setBody($stream); return false; diff --git a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php index c4cd6db190a..1e1e4aaed04 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php +++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php @@ -1,23 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me> - * - * @author Louis Chemineau <louis@chmn.me> - * - * @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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Connector\Sabre\Exception; diff --git a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php index 4fc3399ca81..60b3b06ea01 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php +++ b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre\Exception; diff --git a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php index 83e01f290b8..38708e945e9 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php +++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php @@ -1,40 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Owen Winkler <a_github@midnightcircus.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre\Exception; use Exception; +use OCP\Files\LockNotAcquiredException; class FileLocked extends \Sabre\DAV\Exception { /** * @param string $message * @param int $code */ - public function __construct($message = "", $code = 0, ?Exception $previous = null) { - if ($previous instanceof \OCP\Files\LockNotAcquiredException) { + public function __construct($message = '', $code = 0, ?Exception $previous = null) { + if ($previous instanceof LockNotAcquiredException) { $message = sprintf('Target file %s is locked by another process.', $previous->path); } parent::__construct($message, $code, $previous); diff --git a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php index d71b837ade2..95d4b3ab514 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php +++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @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\Connector\Sabre\Exception; @@ -26,18 +11,16 @@ class Forbidden extends \Sabre\DAV\Exception\Forbidden { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; /** - * @var bool - */ - private $retry; - - /** * @param string $message * @param bool $retry * @param \Exception $previous */ - public function __construct($message, $retry = false, ?\Exception $previous = null) { + public function __construct( + $message, + private $retry = false, + ?\Exception $previous = null, + ) { parent::__construct($message, 0, $previous); - $this->retry = $retry; } /** diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php index a8ca0aea97d..dfc08aa8b88 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php +++ b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * @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\Connector\Sabre\Exception; @@ -29,18 +13,16 @@ class InvalidPath extends Exception { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; /** - * @var bool - */ - private $retry; - - /** * @param string $message * @param bool $retry * @param \Exception|null $previous */ - public function __construct($message, $retry = false, ?\Exception $previous = null) { + public function __construct( + $message, + private $retry = false, + ?\Exception $previous = null, + ) { parent::__construct($message, 0, $previous); - $this->retry = $retry; } /** diff --git a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php index 7c00d15f627..f5cc117fafc 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php +++ b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre\Exception; diff --git a/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php index 1110797aed4..67455fc9474 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php +++ b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre\Exception; diff --git a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php index a7e935d2497..c5fbfa3a16c 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php +++ b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre\Exception; diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php index a195b5722f5..686386dbfef 100644 --- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php +++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -65,6 +45,9 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { // forbidden can be expected when trying to upload to // read-only folders for example Forbidden::class => true, + // our forbidden is expected when access control is blocking + // an item in a folder + \OCA\DAV\Connector\Sabre\Exception\Forbidden::class => true, // Happens when an external storage or federated share is temporarily // not available StorageNotAvailableException::class => true, @@ -84,15 +67,13 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { ServerMaintenanceMode::class => true, ]; - private string $appName; - private LoggerInterface $logger; - /** - * @param string $loggerAppName app name to use when logging + * @param string $appName app name to use when logging */ - public function __construct(string $loggerAppName, LoggerInterface $logger) { - $this->appName = $loggerAppName; - $this->logger = $logger; + public function __construct( + private string $appName, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php index c63b1cf50f2..b0c5a079ce1 100644 --- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -1,33 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Connector\Sabre; +use OCP\AppFramework\Http; use Sabre\DAV\INode; use Sabre\DAV\Locks\LockInfo; use Sabre\DAV\PropFind; +use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\DAV\Xml\Property\LockDiscovery; use Sabre\DAV\Xml\Property\SupportedLock; @@ -47,11 +31,11 @@ use Sabre\HTTP\ResponseInterface; * @package OCA\DAV\Connector\Sabre */ class FakeLockerPlugin extends ServerPlugin { - /** @var \Sabre\DAV\Server */ + /** @var Server */ private $server; /** {@inheritDoc} */ - public function initialize(\Sabre\DAV\Server $server) { + public function initialize(Server $server) { $this->server = $server; $this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1); $this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1); @@ -129,15 +113,15 @@ class FakeLockerPlugin extends ServerPlugin { $lockInfo = new LockInfo(); $lockInfo->token = md5($request->getPath()); $lockInfo->uri = $request->getPath(); - $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY; + $lockInfo->depth = Server::DEPTH_INFINITY; $lockInfo->timeout = 1800; $body = $this->server->xml->write('{DAV:}prop', [ - '{DAV:}lockdiscovery' => - new LockDiscovery([$lockInfo]) + '{DAV:}lockdiscovery' + => new LockDiscovery([$lockInfo]) ]); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setBody($body); return false; @@ -152,7 +136,7 @@ class FakeLockerPlugin extends ServerPlugin { */ public function fakeUnlockProvider(RequestInterface $request, ResponseInterface $response) { - $response->setStatus(204); + $response->setStatus(Http::STATUS_NO_CONTENT); $response->setHeader('Content-Length', '0'); return false; } diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index f54fbbdd15a..d2a71eb3e7b 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -1,40 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Jakob Sack <mail@jakobsack.de> - * @author Jan-Philipp Litza <jplitza@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Owen Winkler <a_github@midnightcircus.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Semih Serhat Karakaya <karakayasemi@itu.edu.tr> - * @author Stefan Schneider <stefan.schneider@squareweave.com.au> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -44,35 +13,38 @@ use OC\Files\Filesystem; use OC\Files\Stream\HashWrapper; use OC\Files\View; use OCA\DAV\AppInfo\Application; -use OCA\DAV\Connector\Sabre\Exception\BadGateway; use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; +use OCP\App\IAppManager; use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Files; use OCP\Files\EntityTooLargeException; use OCP\Files\FileInfo; use OCP\Files\ForbiddenException; use OCP\Files\GenericFileException; +use OCP\Files\IMimeTypeDetector; use OCP\Files\InvalidContentException; use OCP\Files\InvalidPathException; use OCP\Files\LockNotAcquiredException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; -use OCP\Files\Storage; +use OCP\Files\Storage\IWriteStreamStorage; use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; use OCP\L10N\IFactory as IL10NFactory; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; +use OCP\Server; use OCP\Share\IManager; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\NotImplemented; use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\DAV\IFile; @@ -83,8 +55,8 @@ class File extends Node implements IFile { /** * Sets up the node, expects a full path name * - * @param \OC\Files\View $view - * @param \OCP\Files\FileInfo $info + * @param View $view + * @param FileInfo $info * @param ?\OCP\Share\IManager $shareManager * @param ?IRequest $request * @param ?IL10N $l10n @@ -97,14 +69,14 @@ class File extends Node implements IFile { } else { // Querying IL10N directly results in a dependency loop /** @var IL10NFactory $l10nFactory */ - $l10nFactory = \OC::$server->get(IL10NFactory::class); + $l10nFactory = Server::get(IL10NFactory::class); $this->l10n = $l10nFactory->get(Application::APP_ID); } if (isset($request)) { $this->request = $request; } else { - $this->request = \OC::$server->get(IRequest::class); + $this->request = Server::get(IRequest::class); } } @@ -125,7 +97,7 @@ class File extends Node implements IFile { * different object on a subsequent GET you are strongly recommended to not * return an ETag, and just return null. * - * @param resource $data + * @param resource|string $data * * @throws Forbidden * @throws UnsupportedMediaType @@ -149,25 +121,18 @@ class File extends Node implements IFile { // verify path of the target $this->verifyPath(); - // chunked handling - $chunkedHeader = $this->request->getHeader('oc-chunked'); - if ($chunkedHeader) { - try { - return $this->createFileChunked($data); - } catch (\Exception $e) { - $this->convertToSabreException($e); - } - } - - /** @var Storage $partStorage */ [$partStorage] = $this->fileView->resolvePath($this->path); + if ($partStorage === null) { + throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); + } $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); - $view = \OC\Files\Filesystem::getView(); + $view = Filesystem::getView(); if ($needsPartFile) { + $transferId = \rand(); // mark file as partial while uploading (ignored by the scanner) - $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; + $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part'; if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { $needsPartFile = false; @@ -183,10 +148,11 @@ class File extends Node implements IFile { } // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) - /** @var \OC\Files\Storage\Storage $partStorage */ [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath); - /** @var \OC\Files\Storage\Storage $storage */ [$storage, $internalPath] = $this->fileView->resolvePath($this->path); + if ($partStorage === null || $storage === null) { + throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); + } try { if (!$needsPartFile) { try { @@ -220,100 +186,92 @@ class File extends Node implements IFile { if ($this->request->getHeader('X-HASH') !== '') { $hash = $this->request->getHeader('X-HASH'); if ($hash === 'all' || $hash === 'md5') { - $data = HashWrapper::wrap($data, 'md5', function ($hash) { + $data = HashWrapper::wrap($data, 'md5', function ($hash): void { $this->header('X-Hash-MD5: ' . $hash); }); } if ($hash === 'all' || $hash === 'sha1') { - $data = HashWrapper::wrap($data, 'sha1', function ($hash) { + $data = HashWrapper::wrap($data, 'sha1', function ($hash): void { $this->header('X-Hash-SHA1: ' . $hash); }); } if ($hash === 'all' || $hash === 'sha256') { - $data = HashWrapper::wrap($data, 'sha256', function ($hash) { + $data = HashWrapper::wrap($data, 'sha256', function ($hash): void { $this->header('X-Hash-SHA256: ' . $hash); }); } } - if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { + $lengthHeader = $this->request->getHeader('content-length'); + $expected = $lengthHeader !== '' ? (int)$lengthHeader : null; + + if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) { $isEOF = false; - $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { + $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void { $isEOF = feof($stream); }); - $result = true; - $count = -1; - try { - $count = $partStorage->writeStream($internalPartPath, $wrappedData); - } catch (GenericFileException $e) { - $result = false; - } catch (BadGateway $e) { - throw $e; - } - - - if ($result === false) { - $result = $isEOF; - if (is_resource($wrappedData)) { - $result = feof($wrappedData); + $result = is_resource($wrappedData); + if ($result) { + $count = -1; + try { + /** @var IWriteStreamStorage $partStorage */ + $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected); + } catch (GenericFileException $e) { + $logger = Server::get(LoggerInterface::class); + $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']); + $result = $isEOF; + if (is_resource($wrappedData)) { + $result = feof($wrappedData); + } } } } else { $target = $partStorage->fopen($internalPartPath, 'wb'); if ($target === false) { - \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); + Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); // because we have no clue about the cause we can only throw back a 500/Internal Server Error throw new Exception($this->l10n->t('Could not write file contents')); } - [$count, $result] = \OC_Helper::streamCopy($data, $target); + [$count, $result] = Files::streamCopy($data, $target, true); fclose($target); } - - if ($result === false) { - $expected = -1; - $lengthHeader = $this->request->getHeader('content-length'); - if ($lengthHeader) { - $expected = (int)$lengthHeader; - } - if ($expected !== 0) { - throw new Exception( - $this->l10n->t( - 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', - [ - $this->l10n->n('%n byte', '%n bytes', $count), - $this->l10n->n('%n byte', '%n bytes', $expected), - ], - ) - ); - } + if ($result === false && $expected !== null) { + throw new Exception( + $this->l10n->t( + 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', + [ + $this->l10n->n('%n byte', '%n bytes', $count), + $this->l10n->n('%n byte', '%n bytes', $expected), + ], + ) + ); } // if content length is sent by client: // double check if the file was fully received // compare expected and actual size - $lengthHeader = $this->request->getHeader('content-length'); - if ($lengthHeader && $this->request->getMethod() === 'PUT') { - $expected = (int)$lengthHeader; - if ($count !== $expected) { - throw new BadRequest( - $this->l10n->t( - 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.', - [ - $this->l10n->n('%n byte', '%n bytes', $expected), - $this->l10n->n('%n byte', '%n bytes', $count), - ], - ) - ); - } + if ($expected !== null + && $expected !== $count + && $this->request->getMethod() === 'PUT' + ) { + throw new BadRequest( + $this->l10n->t( + 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.', + [ + $this->l10n->n('%n byte', '%n bytes', $expected), + $this->l10n->n('%n byte', '%n bytes', $count), + ], + ) + ); } } catch (\Exception $e) { if ($e instanceof LockedException) { - \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]); + Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]); } else { - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); } if ($needsPartFile) { @@ -354,7 +312,7 @@ class File extends Node implements IFile { $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); $fileExists = $storage->file_exists($internalPath); if ($renameOkay === false || $fileExists === false) { - \OC::$server->get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); + Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); throw new Exception($this->l10n->t('Could not rename part file to final file')); } } catch (ForbiddenException $ex) { @@ -421,11 +379,16 @@ class File extends Node implements IFile { } private function getPartFileBasePath($path) { - $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); + $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true); if ($partFileInStorage) { - return $path; + $filename = basename($path); + // hash does not need to be secure but fast and semi unique + $hashedFilename = hash('xxh128', $filename); + return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename; } else { - return md5($path); // will place it in the root of the view with a unique name + // will place the .part file in the users root directory + // therefor we need to make the name (semi) unique - hash does not need to be secure but fast. + return hash('xxh128', $path); } } @@ -441,19 +404,19 @@ class File extends Node implements IFile { $run = true; if (!$exists) { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [ - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [ + Filesystem::signal_param_path => $hookPath, + Filesystem::signal_param_run => &$run, ]); } else { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [ - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [ + Filesystem::signal_param_path => $hookPath, + Filesystem::signal_param_run => &$run, ]); } - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [ - \OC\Files\Filesystem::signal_param_path => $hookPath, - \OC\Files\Filesystem::signal_param_run => &$run, + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [ + Filesystem::signal_param_path => $hookPath, + Filesystem::signal_param_run => &$run, ]); return $run; } @@ -468,16 +431,16 @@ class File extends Node implements IFile { return; } if (!$exists) { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [ - \OC\Files\Filesystem::signal_param_path => $hookPath + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [ + Filesystem::signal_param_path => $hookPath ]); } else { - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [ - \OC\Files\Filesystem::signal_param_path => $hookPath + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [ + Filesystem::signal_param_path => $hookPath ]); } - \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [ - \OC\Files\Filesystem::signal_param_path => $hookPath + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [ + Filesystem::signal_param_path => $hookPath ]); } @@ -495,20 +458,25 @@ class File extends Node implements IFile { // do a if the file did not exist throw new NotFound(); } + $path = ltrim($this->path, '/'); try { - $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); + $res = $this->fileView->fopen($path, 'rb'); } catch (\Exception $e) { $this->convertToSabreException($e); } if ($res === false) { - throw new ServiceUnavailable($this->l10n->t('Could not open file')); + if ($this->fileView->file_exists($path)) { + throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path])); + } else { + throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path])); + } } // comparing current file size with the one in DB // if different, fix DB and refresh cache. if ($this->getSize() !== $this->fileView->filesize($this->getPath())) { - $logger = \OC::$server->get(LoggerInterface::class); + $logger = Server::get(LoggerInterface::class); $logger->warning('fixing cached size of file id=' . $this->getId()); $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath()); @@ -567,17 +535,16 @@ class File extends Node implements IFile { if ($this->request->getMethod() === 'PROPFIND') { return $mimeType; } - return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); + return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType); } /** * @return array|bool */ public function getDirectDownload() { - if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) { + if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) { return []; } - /** @var \OCP\Files\Storage $storage */ [$storage, $internalPath] = $this->fileView->resolvePath($this->path); if (is_null($storage)) { return []; @@ -587,135 +554,6 @@ class File extends Node implements IFile { } /** - * @param resource $data - * @return null|string - * @throws Exception - * @throws BadRequest - * @throws NotImplemented - * @throws ServiceUnavailable - */ - private function createFileChunked($data) { - [$path, $name] = \Sabre\Uri\split($this->path); - - $info = \OC_FileChunking::decodeName($name); - if (empty($info)) { - throw new NotImplemented($this->l10n->t('Invalid chunk name')); - } - - $chunk_handler = new \OC_FileChunking($info); - $bytesWritten = $chunk_handler->store($info['index'], $data); - - //detect aborted upload - if ($this->request->getMethod() === 'PUT') { - $lengthHeader = $this->request->getHeader('content-length'); - if ($lengthHeader) { - $expected = (int)$lengthHeader; - if ($bytesWritten !== $expected) { - $chunk_handler->remove($info['index']); - throw new BadRequest( - $this->l10n->t( - 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.', - [ - $this->l10n->n('%n byte', '%n bytes', $expected), - $this->l10n->n('%n byte', '%n bytes', $bytesWritten), - ], - ) - ); - } - } - } - - if ($chunk_handler->isComplete()) { - /** @var Storage $storage */ - [$storage,] = $this->fileView->resolvePath($path); - $needsPartFile = $storage->needsPartFile(); - $partFile = null; - - $targetPath = $path . '/' . $info['name']; - /** @var \OC\Files\Storage\Storage $targetStorage */ - [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath); - - $exists = $this->fileView->file_exists($targetPath); - - try { - $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); - - $this->emitPreHooks($exists, $targetPath); - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); - /** @var \OC\Files\Storage\Storage $targetStorage */ - [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath); - - if ($needsPartFile) { - // we first assembly the target file as a part file - $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; - /** @var \OC\Files\Storage\Storage $targetStorage */ - [$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile); - - - $chunk_handler->file_assemble($partStorage, $partInternalPath); - - // here is the final atomic rename - $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); - $fileExists = $targetStorage->file_exists($targetInternalPath); - if ($renameOkay === false || $fileExists === false) { - \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']); - // only delete if an error occurred and the target file was already created - if ($fileExists) { - // set to null to avoid double-deletion when handling exception - // stray part file - $partFile = null; - $targetStorage->unlink($targetInternalPath); - } - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); - throw new Exception($this->l10n->t('Could not rename part file assembled from chunks')); - } - } else { - // assemble directly into the final file - $chunk_handler->file_assemble($targetStorage, $targetInternalPath); - } - - // allow sync clients to send the mtime along in a header - $mtimeHeader = $this->request->getHeader('x-oc-mtime'); - if ($mtimeHeader !== '') { - $mtime = $this->sanitizeMtime($mtimeHeader); - if ($targetStorage->touch($targetInternalPath, $mtime)) { - $this->header('X-OC-MTime: accepted'); - } - } - - // since we skipped the view we need to scan and emit the hooks ourselves - $targetStorage->getUpdater()->update($targetInternalPath); - - $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); - - $this->emitPostHooks($exists, $targetPath); - - // FIXME: should call refreshInfo but can't because $this->path is not the of the final file - $info = $this->fileView->getFileInfo($targetPath); - - $checksumHeader = $this->request->getHeader('oc-checksum'); - if ($checksumHeader) { - $checksum = trim($checksumHeader); - $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); - } elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') { - $this->fileView->putFileInfo($this->path, ['checksum' => '']); - } - - $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); - - return $info->getEtag(); - } catch (\Exception $e) { - if ($partFile !== null) { - $targetStorage->unlink($targetInternalPath); - } - $this->convertToSabreException($e); - } - } - - return null; - } - - /** * Convert the given exception to a SabreException instance * * @param \Exception $e diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 7c9c7099878..843383a0452 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -1,42 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Michael Jobst <mjobst+github@tecratech.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; use OC\AppFramework\Http\Request; +use OC\FilesMetadata\Model\FilesMetadata; +use OC\User\NoUserException; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\Files_Sharing\External\Mount as SharingExternalMount; +use OCP\Accounts\IAccountManager; use OCP\Constants; use OCP\Files\ForbiddenException; +use OCP\Files\IFilenameValidator; +use OCP\Files\InvalidPathException; +use OCP\Files\Storage\ISharedStorage; use OCP\Files\StorageNotAvailableException; use OCP\FilesMetadata\Exceptions\FilesMetadataException; use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; @@ -46,6 +27,7 @@ use OCP\IConfig; use OCP\IPreview; use OCP\IRequest; use OCP\IUserSession; +use OCP\L10N\IFactory; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\IFile; @@ -80,11 +62,12 @@ class FilesPlugin extends ServerPlugin { public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview'; public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type'; public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root'; - public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted'; + public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated'; public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag'; public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time'; public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time'; public const SHARE_NOTE = '{http://nextcloud.org/ns}note'; + public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download'; public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count'; public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count'; public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-'; @@ -92,33 +75,28 @@ class FilesPlugin extends ServerPlugin { /** Reference to main server object */ private ?Server $server = null; - private Tree $tree; - private IUserSession $userSession; /** - * Whether this is public webdav. - * If true, some returned information will be stripped off. + * @param Tree $tree + * @param IConfig $config + * @param IRequest $request + * @param IPreview $previewManager + * @param IUserSession $userSession + * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off. + * @param bool $downloadAttachment + * @return void */ - private bool $isPublic; - private bool $downloadAttachment; - private IConfig $config; - private IRequest $request; - private IPreview $previewManager; - - public function __construct(Tree $tree, - IConfig $config, - IRequest $request, - IPreview $previewManager, - IUserSession $userSession, - bool $isPublic = false, - bool $downloadAttachment = true) { - $this->tree = $tree; - $this->config = $config; - $this->request = $request; - $this->userSession = $userSession; - $this->isPublic = $isPublic; - $this->downloadAttachment = $downloadAttachment; - $this->previewManager = $previewManager; + public function __construct( + private Tree $tree, + private IConfig $config, + private IRequest $request, + private IPreview $previewManager, + private IUserSession $userSession, + private IFilenameValidator $validator, + private IAccountManager $accountManager, + private bool $isPublic = false, + private bool $downloadAttachment = true, + ) { } /** @@ -148,7 +126,7 @@ class FilesPlugin extends ServerPlugin { $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME; $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME; $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME; - $server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME; + $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME; $server->protectedProperties[] = self::SHARE_NOTE; // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH @@ -162,40 +140,79 @@ class FilesPlugin extends ServerPlugin { $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); $this->server->on('afterMethod:GET', [$this,'httpGet']); $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); - $this->server->on('afterResponse', function ($request, ResponseInterface $response) { + $this->server->on('afterResponse', function ($request, ResponseInterface $response): void { $body = $response->getBody(); if (is_resource($body)) { fclose($body); } }); $this->server->on('beforeMove', [$this, 'checkMove']); + $this->server->on('beforeCopy', [$this, 'checkCopy']); } /** - * Plugin that checks if a move can actually be performed. + * Plugin that checks if a copy can actually be performed. * * @param string $source source path - * @param string $destination destination path - * @throws Forbidden - * @throws NotFound + * @param string $target target path + * @throws NotFound If the source does not exist + * @throws InvalidPath If the target is invalid */ - public function checkMove($source, $destination) { + public function checkCopy($source, $target): void { $sourceNode = $this->tree->getNodeForPath($source); if (!$sourceNode instanceof Node) { return; } - [$sourceDir,] = \Sabre\Uri\split($source); - [$destinationDir,] = \Sabre\Uri\split($destination); - if ($sourceDir !== $destinationDir) { - $sourceNodeFileInfo = $sourceNode->getFileInfo(); - if ($sourceNodeFileInfo === null) { - throw new NotFound($source . ' does not exist'); + // Ensure source exists + $sourceNodeFileInfo = $sourceNode->getFileInfo(); + if ($sourceNodeFileInfo === null) { + throw new NotFound($source . ' does not exist'); + } + // Ensure the target name is valid + try { + [$targetPath, $targetName] = \Sabre\Uri\split($target); + $this->validator->validateFilename($targetName); + } catch (InvalidPathException $e) { + throw new InvalidPath($e->getMessage(), false); + } + // Ensure the target path is valid + $segments = array_slice(explode('/', $targetPath), 2); + foreach ($segments as $segment) { + if ($this->validator->isFilenameValid($segment) === false) { + $l = \OCP\Server::get(IFactory::class)->get('dav'); + throw new InvalidPath($l->t('Invalid target path')); } + } + } - if (!$sourceNodeFileInfo->isDeletable()) { - throw new Forbidden($source . " cannot be deleted"); - } + /** + * Plugin that checks if a move can actually be performed. + * + * @param string $source source path + * @param string $target target path + * @throws Forbidden If the source is not deletable + * @throws NotFound If the source does not exist + * @throws InvalidPath If the target name is invalid + */ + public function checkMove(string $source, string $target): void { + $sourceNode = $this->tree->getNodeForPath($source); + if (!$sourceNode instanceof Node) { + return; + } + + // First check copyable (move only needs additional delete permission) + $this->checkCopy($source, $target); + + // The source needs to be deletable for moving + $sourceNodeFileInfo = $sourceNode->getFileInfo(); + if (!$sourceNodeFileInfo->isDeletable()) { + throw new Forbidden($source . ' cannot be deleted'); + } + + // The source is not allowed to be the parent of the target + if (str_starts_with($source, $target . '/')) { + throw new Forbidden($source . ' cannot be moved to it\'s parent'); } } @@ -240,8 +257,8 @@ class FilesPlugin extends ServerPlugin { // adds a 'Content-Disposition: attachment' header in case no disposition // header has been set before - if ($this->downloadAttachment && - $response->getHeader('Content-Disposition') === null) { + if ($this->downloadAttachment + && $response->getHeader('Content-Disposition') === null) { $filename = $node->getName(); if ($this->request->isUserAgent( [ @@ -256,7 +273,7 @@ class FilesPlugin extends ServerPlugin { } } - if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + if ($node instanceof File) { //Add OC-Checksum header $checksum = $node->getChecksum(); if ($checksum !== null && $checksum !== '') { @@ -276,7 +293,7 @@ class FilesPlugin extends ServerPlugin { public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { $httpRequest = $this->server->httpRequest; - if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { + if ($node instanceof Node) { /** * This was disabled, because it made dir listing throw an exception, * so users were unable to navigate into folders where one subitem @@ -347,9 +364,32 @@ class FilesPlugin extends ServerPlugin { $owner = $node->getOwner(); if (!$owner) { return null; - } else { + } + + // Get current user to see if we're in a public share or not + $user = $this->userSession->getUser(); + + // If the user is logged in, we can return the display name + if ($user !== null) { + return $owner->getDisplayName(); + } + + // Check if the user published their display name + try { + $ownerAccount = $this->accountManager->getAccount($owner); + } catch (NoUserException) { + // do not lock process if owner is not local + return null; + } + + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); + + // Since we are not logged in, we need to have at least the published scope + if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { return $owner->getDisplayName(); } + + return null; }); $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) { @@ -372,17 +412,27 @@ class FilesPlugin extends ServerPlugin { return $node->getNode()->getInternalPath() === '' ? 'true' : 'false'; }); - $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest): ?string { + $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string { $user = $this->userSession->getUser(); - if ($user === null) { - return null; - } return $node->getNoteFromShare( - $user->getUID() + $user?->getUID() ); }); - $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) { + $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) { + $storage = $node->getNode()->getStorage(); + if ($storage->instanceOfStorage(ISharedStorage::class)) { + /** @var ISharedStorage $storage */ + return match($storage->getShare()->getHideDownload()) { + true => 'true', + false => 'false', + }; + } else { + return null; + } + }); + + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () { return $this->config->getSystemValue('data-fingerprint', ''); }); $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) { @@ -413,9 +463,14 @@ class FilesPlugin extends ServerPlugin { $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) { return $node->getName(); }); + + $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) { + return $node->getFileInfo()->getMountPoint() + instanceof SharingExternalMount; + }); } - if ($node instanceof \OCA\DAV\Connector\Sabre\File) { + if ($node instanceof File) { $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) { try { $directDownloadUrl = $node->getDirectDownload(); @@ -449,10 +504,6 @@ class FilesPlugin extends ServerPlugin { return $node->getSize(); }); - $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) { - return $node->getFileInfo()->isEncrypted() ? '1' : '0'; - }); - $requestProperties = $propFind->getRequestedProperties(); if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true) @@ -490,8 +541,8 @@ class FilesPlugin extends ServerPlugin { $ocmPermissions[] = 'read'; } - if (($ncPermissions & Constants::PERMISSION_CREATE) || - ($ncPermissions & Constants::PERMISSION_UPDATE)) { + if (($ncPermissions & Constants::PERMISSION_CREATE) + || ($ncPermissions & Constants::PERMISSION_UPDATE)) { $ocmPermissions[] = 'write'; } @@ -508,7 +559,7 @@ class FilesPlugin extends ServerPlugin { */ public function handleUpdateProperties($path, PropPatch $propPatch) { $node = $this->tree->getNodeForPath($path); - if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) { + if (!($node instanceof Node)) { return; } @@ -537,7 +588,7 @@ class FilesPlugin extends ServerPlugin { if (empty($time)) { return false; } - $node->setCreationTime((int) $time); + $node->setCreationTime((int)$time); return true; }); @@ -579,7 +630,9 @@ class FilesPlugin extends ServerPlugin { $propPatch->handle( $mutation, function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool { + /** @var FilesMetadata $metadata */ $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true); + $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId()); $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX)); // confirm metadata key is editable via PROPPATCH @@ -587,6 +640,12 @@ class FilesPlugin extends ServerPlugin { throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node'); } + if ($value === null) { + $metadata->unset($metadataKey); + $filesMetadataManager->saveMetadata($metadata); + return true; + } + // If the metadata is unknown, it defaults to string. try { $type = $knownMetadata->getType($metadataKey); @@ -660,8 +719,6 @@ class FilesPlugin extends ServerPlugin { return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION; } - - /** * @param string $filePath * @param ?\Sabre\DAV\INode $node @@ -669,25 +726,16 @@ class FilesPlugin extends ServerPlugin { * @throws \Sabre\DAV\Exception\BadRequest */ public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) { - // chunked upload handling - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - [$path, $name] = \Sabre\Uri\split($filePath); - $info = \OC_FileChunking::decodeName($name); - if (!empty($info)) { - $filePath = $path . '/' . $info['name']; - } - } - // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder - if (!$this->server->tree->nodeExists($filePath)) { - return; - } - $node = $this->server->tree->getNodeForPath($filePath); - if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { - $fileId = $node->getFileId(); - if (!is_null($fileId)) { - $this->server->httpResponse->setHeader('OC-FileId', $fileId); + try { + $node = $this->server->tree->getNodeForPath($filePath); + if ($node instanceof Node) { + $fileId = $node->getFileId(); + if (!is_null($fileId)) { + $this->server->httpResponse->setHeader('OC-FileId', $fileId); + } } + } catch (NotFound) { } } } diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php index 6a8cb3f0f59..b59d1373af5 100644 --- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php @@ -1,34 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Vinicius Cubas Brand <vinicius@eita.org.br> - * - * @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\Connector\Sabre; use OC\Files\View; +use OCA\Circles\Api\v1\Circles; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\Files\Folder; use OCP\Files\Node as INode; use OCP\IGroupManager; @@ -61,55 +43,8 @@ class FilesReportPlugin extends ServerPlugin { private $server; /** - * @var Tree - */ - private $tree; - - /** - * @var View - */ - private $fileView; - - /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var ISystemTagObjectMapper - */ - private $tagMapper; - - /** - * Manager for private tags - * - * @var ITagManager - */ - private $fileTagger; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var Folder - */ - private $userFolder; - - /** - * @var IAppManager - */ - private $appManager; - - /** * @param Tree $tree - * @param View $view + * @param View $fileView * @param ISystemTagManager $tagManager * @param ISystemTagObjectMapper $tagMapper * @param ITagManager $fileTagger manager for private tags @@ -118,25 +53,20 @@ class FilesReportPlugin extends ServerPlugin { * @param Folder $userFolder * @param IAppManager $appManager */ - public function __construct(Tree $tree, - View $view, - ISystemTagManager $tagManager, - ISystemTagObjectMapper $tagMapper, - ITagManager $fileTagger, - IUserSession $userSession, - IGroupManager $groupManager, - Folder $userFolder, - IAppManager $appManager + public function __construct( + private Tree $tree, + private View $fileView, + private ISystemTagManager $tagManager, + private ISystemTagObjectMapper $tagMapper, + /** + * Manager for private tags + */ + private ITagManager $fileTagger, + private IUserSession $userSession, + private IGroupManager $groupManager, + private Folder $userFolder, + private IAppManager $appManager, ) { - $this->tree = $tree; - $this->fileView = $view; - $this->tagManager = $tagManager; - $this->tagMapper = $tagMapper; - $this->fileTagger = $fileTagger; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->userFolder = $userFolder; - $this->appManager = $appManager; } /** @@ -226,7 +156,7 @@ class FilesReportPlugin extends ServerPlugin { // to user backends. I.e. the final result may return more results than requested. $resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null); } catch (TagNotFoundException $e) { - throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); + throw new PreconditionFailed('Cannot filter by non-existing tag'); } $results = []; @@ -254,7 +184,7 @@ class FilesReportPlugin extends ServerPlugin { new MultiStatus($responses) ); - $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setBody($xml); @@ -375,7 +305,7 @@ class FilesReportPlugin extends ServerPlugin { if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) { return []; } - return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds); + return Circles::getFilesForCircles($circlesIds); } @@ -383,7 +313,7 @@ class FilesReportPlugin extends ServerPlugin { * Prepare propfind response for the given nodes * * @param string $filesUri $filesUri URI leading to root of the files URI, - * with a leading slash but no trailing slash + * with a leading slash but no trailing slash * @param string[] $requestedProps requested properties * @param Node[] nodes nodes for which to fetch and prepare responses * @return Response[] @@ -430,7 +360,7 @@ class FilesReportPlugin extends ServerPlugin { $results = []; foreach ($fileIds as $fileId) { - $entry = $folder->getFirstNodeById($fileId); + $entry = $folder->getFirstNodeById((int)$fileId); if ($entry) { $results[] = $this->wrapNode($entry); } @@ -439,7 +369,7 @@ class FilesReportPlugin extends ServerPlugin { return $results; } - protected function wrapNode(\OCP\Files\Node $node): File|Directory { + protected function wrapNode(INode $node): File|Directory { if ($node instanceof \OCP\Files\File) { return new File($this->fileView, $node); } else { diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php index 6305b0ec138..6640771dc31 100644 --- a/apps/dav/lib/Connector/Sabre/LockPlugin.php +++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jaakko Salo <jaakkos@gmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @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\Connector\Sabre; @@ -61,7 +42,7 @@ class LockPlugin extends ServerPlugin { public function getLock(RequestInterface $request) { // we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree // so instead we limit ourselves to the PUT method manually - if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + if ($request->getMethod() !== 'PUT') { return; } try { @@ -84,7 +65,7 @@ class LockPlugin extends ServerPlugin { if ($this->isLocked === false) { return; } - if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) { + if ($request->getMethod() !== 'PUT') { return; } try { diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php index 1fc02320805..d5ab7f09dfa 100644 --- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php +++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -36,10 +16,7 @@ use Sabre\DAV\ServerPlugin; class MaintenancePlugin extends ServerPlugin { - /** @var IConfig */ - private $config; - - /** @var \OCP\IL10N */ + /** @var IL10N */ private $l10n; /** @@ -52,8 +29,10 @@ class MaintenancePlugin extends ServerPlugin { /** * @param IConfig $config */ - public function __construct(IConfig $config, IL10N $l10n) { - $this->config = $config; + public function __construct( + private IConfig $config, + IL10N $l10n, + ) { $this->l10n = \OC::$server->getL10N('dav'); } diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php index 6700b1eb81b..e18ef58149a 100644 --- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php +++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php @@ -1,23 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me> - * - * @author Louis Chemineau <louis@chmn.me> - * - * @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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Connector\Sabre; diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index 2b77dc08e8d..505e6b5eda4 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -1,37 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Klaas Freitag <freitag@owncloud.com> - * @author Markus Goetz <markus@woboq.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -40,20 +12,20 @@ use OC\Files\Node\File; use OC\Files\Node\Folder; use OC\Files\View; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCP\Constants; use OCP\Files\DavUtil; use OCP\Files\FileInfo; +use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; use OCP\Files\StorageNotAvailableException; +use OCP\Server; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; abstract class Node implements \Sabre\DAV\INode { /** - * @var View - */ - protected $fileView; - - /** * The path to the current node * * @var string @@ -79,21 +51,24 @@ abstract class Node implements \Sabre\DAV\INode { /** * Sets up the node, expects a full path name */ - public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null) { - $this->fileView = $view; + public function __construct( + protected View $fileView, + FileInfo $info, + ?IManager $shareManager = null, + ) { $this->path = $this->fileView->getRelativePath($info->getPath()); $this->info = $info; if ($shareManager) { $this->shareManager = $shareManager; } else { - $this->shareManager = \OC::$server->getShareManager(); + $this->shareManager = Server::get(\OCP\Share\IManager::class); } if ($info instanceof Folder || $info instanceof File) { $this->node = $info; } else { // The Node API assumes that the view passed doesn't have a fake root - $rootView = \OC::$server->get(View::class); - $root = \OC::$server->get(IRootFolder::class); + $rootView = Server::get(View::class); + $root = Server::get(IRootFolder::class); if ($info->getType() === FileInfo::TYPE_FOLDER) { $this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info); } else { @@ -105,11 +80,11 @@ abstract class Node implements \Sabre\DAV\INode { protected function refreshInfo(): void { $info = $this->fileView->getFileInfo($this->path); if ($info === false) { - throw new \Sabre\DAV\Exception('Failed to get fileinfo for '. $this->path); + throw new \Sabre\DAV\Exception('Failed to get fileinfo for ' . $this->path); } $this->info = $info; - $root = \OC::$server->get(IRootFolder::class); - $rootView = \OC::$server->get(View::class); + $root = Server::get(IRootFolder::class); + $rootView = Server::get(View::class); if ($this->info->getType() === FileInfo::TYPE_FOLDER) { $this->node = new Folder($root, $rootView, $this->path, $this->info); } else { @@ -143,21 +118,21 @@ abstract class Node implements \Sabre\DAV\INode { * @throws \Sabre\DAV\Exception\Forbidden */ public function setName($name) { - // rename is only allowed if the update privilege is granted - if (!($this->info->isUpdateable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) { + // rename is only allowed if the delete privilege is granted + // (basically rename is a copy with delete of the original node) + if (!($this->info->isDeletable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) { throw new \Sabre\DAV\Exception\Forbidden(); } [$parentPath,] = \Sabre\Uri\split($this->path); [, $newName] = \Sabre\Uri\split($name); + $newPath = $parentPath . '/' . $newName; // verify path of the target - $this->verifyPath(); - - $newPath = $parentPath . '/' . $newName; + $this->verifyPath($newPath); if (!$this->fileView->rename($this->path, $newPath)) { - throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath); + throw new \Sabre\DAV\Exception('Failed to rename ' . $this->path . ' to ' . $newPath); } $this->path = $newPath; @@ -289,8 +264,8 @@ abstract class Node implements \Sabre\DAV\INode { $storage = null; } - if ($storage && $storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { - /** @var \OCA\Files_Sharing\SharedStorage $storage */ + if ($storage && $storage->instanceOfStorage(ISharedStorage::class)) { + /** @var ISharedStorage $storage */ $permissions = (int)$storage->getShare()->getPermissions(); } else { $permissions = $this->info->getPermissions(); @@ -308,15 +283,15 @@ abstract class Node implements \Sabre\DAV\INode { } if (!$mountpoint->getOption('readonly', false) && $mountpointpath === $this->info->getPath()) { - $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE; + $permissions |= Constants::PERMISSION_DELETE | Constants::PERMISSION_UPDATE; } } /* * Files can't have create or delete permissions */ - if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) { - $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE); + if ($this->info->getType() === FileInfo::TYPE_FILE) { + $permissions &= ~(Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE); } return $permissions; @@ -326,16 +301,15 @@ abstract class Node implements \Sabre\DAV\INode { * @return array */ public function getShareAttributes(): array { - $attributes = []; - try { - $storage = $this->info->getStorage(); - } catch (StorageNotAvailableException $e) { - $storage = null; + $storage = $this->node->getStorage(); + } catch (NotFoundException $e) { + return []; } - if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) { - /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $attributes = []; + if ($storage->instanceOfStorage(ISharedStorage::class)) { + /** @var ISharedStorage $storage */ $attributes = $storage->getShare()->getAttributes(); if ($attributes === null) { return []; @@ -347,29 +321,24 @@ abstract class Node implements \Sabre\DAV\INode { return $attributes; } - /** - * @param string $user - * @return string - */ - public function getNoteFromShare($user) { - if ($user === null) { - return ''; + public function getNoteFromShare(?string $user): ?string { + try { + $storage = $this->node->getStorage(); + } catch (NotFoundException) { + return null; } - // Retrieve note from the share object already loaded into - // memory, to avoid additional database queries. - $storage = $this->getNode()->getStorage(); - if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) { - return ''; + if ($storage->instanceOfStorage(ISharedStorage::class)) { + /** @var ISharedStorage $storage */ + $share = $storage->getShare(); + if ($user === $share->getShareOwner()) { + // Note is only for recipient not the owner + return null; + } + return $share->getNote(); } - /** @var \OCA\Files_Sharing\SharedStorage $storage */ - $share = $storage->getShare(); - $note = $share->getNote(); - if ($share->getShareOwner() !== $user) { - return $note; - } - return ''; + return null; } /** @@ -383,11 +352,14 @@ abstract class Node implements \Sabre\DAV\INode { return $this->info->getOwner(); } - protected function verifyPath() { + protected function verifyPath(?string $path = null): void { try { - $fileName = basename($this->info->getPath()); - $this->fileView->verifyPath($this->path, $fileName); - } catch (\OCP\Files\InvalidPathException $ex) { + $path = $path ?? $this->info->getPath(); + $this->fileView->verifyPath( + dirname($path), + basename($path), + ); + } catch (InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); } } @@ -421,7 +393,7 @@ abstract class Node implements \Sabre\DAV\INode { return $this->node; } - protected function sanitizeMtime($mtimeFromRequest) { + protected function sanitizeMtime(string $mtimeFromRequest): int { return MtimeSanitizer::sanitizeMtime($mtimeFromRequest); } } diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php index c129371e376..bfbdfb33db0 100644 --- a/apps/dav/lib/Connector/Sabre/ObjectTree.php +++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php @@ -1,39 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; use OC\Files\FileInfo; use OC\Files\Storage\FailedStorage; +use OC\Files\Storage\Storage; +use OC\Files\View; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCP\Files\ForbiddenException; +use OCP\Files\InvalidPathException; +use OCP\Files\Mount\IMountManager; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; use OCP\Lock\LockedException; @@ -41,12 +24,12 @@ use OCP\Lock\LockedException; class ObjectTree extends CachingTree { /** - * @var \OC\Files\View + * @var View */ protected $fileView; /** - * @var \OCP\Files\Mount\IMountManager + * @var IMountManager */ protected $mountManager; @@ -58,45 +41,16 @@ class ObjectTree extends CachingTree { /** * @param \Sabre\DAV\INode $rootNode - * @param \OC\Files\View $view - * @param \OCP\Files\Mount\IMountManager $mountManager + * @param View $view + * @param IMountManager $mountManager */ - public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) { + public function init(\Sabre\DAV\INode $rootNode, View $view, IMountManager $mountManager) { $this->rootNode = $rootNode; $this->fileView = $view; $this->mountManager = $mountManager; } /** - * If the given path is a chunked file name, converts it - * to the real file name. Only applies if the OC-CHUNKED header - * is present. - * - * @param string $path chunk file path to convert - * - * @return string path to real file - */ - private function resolveChunkFile($path) { - if (isset($_SERVER['HTTP_OC_CHUNKED'])) { - // resolve to real file name to find the proper node - [$dir, $name] = \Sabre\Uri\split($path); - if ($dir === '/' || $dir === '.') { - $dir = ''; - } - - $info = \OC_FileChunking::decodeName($name); - // only replace path if it was really the chunked file - if (isset($info['transferid'])) { - // getNodePath is called for multiple nodes within a chunk - // upload call - $path = $dir . '/' . $info['name']; - $path = ltrim($path, '/'); - } - } - return $path; - } - - /** * Returns the INode object for the requested path * * @param string $path @@ -120,7 +74,7 @@ class ObjectTree extends CachingTree { if ($path) { try { $this->fileView->verifyPath($path, basename($path)); - } catch (\OCP\Files\InvalidPathException $ex) { + } catch (InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); } } @@ -138,7 +92,7 @@ class ObjectTree extends CachingTree { $internalPath = $mount->getInternalPath($absPath); if ($storage && $storage->file_exists($internalPath)) { /** - * @var \OC\Files\Storage\Storage $storage + * @var Storage $storage */ // get data directly $data = $storage->getMetaData($internalPath); @@ -147,9 +101,6 @@ class ObjectTree extends CachingTree { $info = null; } } else { - // resolve chunk file name to real name, if applicable - $path = $this->resolveChunkFile($path); - // read from cache try { $info = $this->fileView->getFileInfo($path); @@ -173,9 +124,9 @@ class ObjectTree extends CachingTree { } if ($info->getType() === 'dir') { - $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this); + $node = new Directory($this->fileView, $info, $this); } else { - $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); + $node = new File($this->fileView, $info); } $this->cache[$path] = $node; @@ -222,7 +173,7 @@ class ObjectTree extends CachingTree { [$destinationDir, $destinationName] = \Sabre\Uri\split($destinationPath); try { $this->fileView->verifyPath($destinationDir, $destinationName); - } catch (\OCP\Files\InvalidPathException $ex) { + } catch (InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); } diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index c6f9fe3affc..d6ea9fd887d 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -1,44 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2018, Georg Ehrke - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Seitz <christoph.seitz@posteo.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Vinicius Cubas Brand <vinicius@eita.org.br> - * - * @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: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Connector\Sabre; use OC\KnownUser\KnownUserService; +use OCA\Circles\Api\v1\Circles; use OCA\Circles\Exceptions\CircleNotFoundException; +use OCA\Circles\Model\Circle; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\Traits\PrincipalProxyTrait; use OCP\Accounts\IAccountManager; @@ -61,24 +33,6 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface; class Principal implements BackendInterface { - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IAccountManager */ - private $accountManager; - - /** @var IShareManager */ - private $shareManager; - - /** @var IUserSession */ - private $userSession; - - /** @var IAppManager */ - private $appManager; - /** @var string */ private $principalPrefix; @@ -88,40 +42,25 @@ class Principal implements BackendInterface { /** @var bool */ private $hasCircles; - /** @var ProxyMapper */ - private $proxyMapper; - /** @var KnownUserService */ private $knownUserService; - /** @var IConfig */ - private $config; - /** @var IFactory */ - private $languageFactory; - - public function __construct(IUserManager $userManager, - IGroupManager $groupManager, - IAccountManager $accountManager, - IShareManager $shareManager, - IUserSession $userSession, - IAppManager $appManager, - ProxyMapper $proxyMapper, + public function __construct( + private IUserManager $userManager, + private IGroupManager $groupManager, + private IAccountManager $accountManager, + private IShareManager $shareManager, + private IUserSession $userSession, + private IAppManager $appManager, + private ProxyMapper $proxyMapper, KnownUserService $knownUserService, - IConfig $config, - IFactory $languageFactory, - string $principalPrefix = 'principals/users/') { - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->accountManager = $accountManager; - $this->shareManager = $shareManager; - $this->userSession = $userSession; - $this->appManager = $appManager; + private IConfig $config, + private IFactory $languageFactory, + string $principalPrefix = 'principals/users/', + ) { $this->principalPrefix = trim($principalPrefix, '/'); $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/'); - $this->proxyMapper = $proxyMapper; $this->knownUserService = $knownUserService; - $this->config = $config; - $this->languageFactory = $languageFactory; } use PrincipalProxyTrait { @@ -211,7 +150,12 @@ class Principal implements BackendInterface { } elseif ($prefix === 'principals/system') { return [ 'uri' => 'principals/system/' . $name, - '{DAV:}displayname' => $this->languageFactory->get('dav')->t("Accounts"), + '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'), + ]; + } elseif ($prefix === 'principals/shares') { + return [ + 'uri' => 'principals/shares/' . $name, + '{DAV:}displayname' => $name, ]; } return null; @@ -242,6 +186,9 @@ class Principal implements BackendInterface { if ($this->hasGroups || $needGroups) { $userGroups = $this->groupManager->getUserGroups($user); foreach ($userGroups as $userGroup) { + if ($userGroup->hideFromCollaboration()) { + continue; + } $groups[] = 'principals/groups/' . urlencode($userGroup->getGID()); } } @@ -561,7 +508,7 @@ class Principal implements BackendInterface { } try { - $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true); + $circle = Circles::detailsCircle($circleUniqueId, true); } catch (QueryException $ex) { return null; } catch (CircleNotFoundException $ex) { @@ -586,7 +533,7 @@ class Principal implements BackendInterface { * @param string $principal * @return array * @throws Exception - * @throws \OCP\AppFramework\QueryException + * @throws QueryException * @suppress PhanUndeclaredClassMethod */ public function getCircleMembership($principal):array { @@ -601,10 +548,10 @@ class Principal implements BackendInterface { throw new Exception('Principal not found'); } - $circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true); + $circles = Circles::joinedCircles($name, true); $circles = array_map(function ($circle) { - /** @var \OCA\Circles\Model\Circle $circle */ + /** @var Circle $circle */ return 'principals/circles/' . urlencode($circle->getSingleId()); }, $circles); diff --git a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php new file mode 100644 index 00000000000..130d4562146 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types = 1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\Server as SabreServer; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * This plugin runs after requests and logs an error if a plugin is detected + * to be doing too many SQL requests. + */ +class PropFindMonitorPlugin extends ServerPlugin { + + /** + * A Plugin can scan up to this amount of nodes without an error being + * reported. + */ + public const THRESHOLD_NODES = 50; + + /** + * A plugin can use up to this amount of queries per node. + */ + public const THRESHOLD_QUERY_FACTOR = 1; + + private SabreServer $server; + + public function initialize(SabreServer $server): void { + $this->server = $server; + $this->server->on('afterResponse', [$this, 'afterResponse']); + } + + public function afterResponse( + RequestInterface $request, + ResponseInterface $response): void { + if (!$this->server instanceof Server) { + return; + } + + $pluginQueries = $this->server->getPluginQueries(); + if (empty($pluginQueries)) { + return; + } + $maxDepth = max(0, ...array_keys($pluginQueries)); + // entries at the top are usually not interesting + unset($pluginQueries[$maxDepth]); + + $logger = $this->server->getLogger(); + foreach ($pluginQueries as $depth => $propFinds) { + foreach ($propFinds as $pluginName => $propFind) { + [ + 'queries' => $queries, + 'nodes' => $nodes + ] = $propFind; + if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES + || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { + continue; + } + $logger->error( + '{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [ + 'name' => $pluginName, + 'scans' => $nodes, + 'count' => $queries, + 'depth' => $depth, + 'maxDepth' => $maxDepth, + ] + ); + } + } + } +} diff --git a/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php index 0c274d807a0..15daf1f34b6 100644 --- a/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php +++ b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php index d5b3d41e1ef..2ca1c25e2f6 100644 --- a/apps/dav/lib/Connector/Sabre/PublicAuth.php +++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php @@ -2,40 +2,21 @@ declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; +use OCP\Defaults; use OCP\IRequest; use OCP\ISession; +use OCP\IURLGenerator; use OCP\Security\Bruteforce\IThrottler; +use OCP\Security\Bruteforce\MaxDelayReached; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; use OCP\Share\IShare; @@ -43,6 +24,7 @@ use Psr\Log\LoggerInterface; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Exception\NotAuthenticated; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\PreconditionFailed; use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\HTTP; use Sabre\HTTP\RequestInterface; @@ -58,40 +40,33 @@ class PublicAuth extends AbstractBasic { public const DAV_AUTHENTICATED = 'public_link_authenticated'; private ?IShare $share = null; - private IManager $shareManager; - private ISession $session; - private IRequest $request; - private IThrottler $throttler; - private LoggerInterface $logger; - - public function __construct(IRequest $request, - IManager $shareManager, - ISession $session, - IThrottler $throttler, - LoggerInterface $logger) { - $this->request = $request; - $this->shareManager = $shareManager; - $this->session = $session; - $this->throttler = $throttler; - $this->logger = $logger; + public function __construct( + private IRequest $request, + private IManager $shareManager, + private ISession $session, + private IThrottler $throttler, + private LoggerInterface $logger, + private IURLGenerator $urlGenerator, + ) { // setup realm - $defaults = new \OCP\Defaults(); + $defaults = new Defaults(); $this->realm = $defaults->getName(); } /** - * @param RequestInterface $request - * @param ResponseInterface $response - * - * @return array * @throws NotAuthenticated + * @throws MaxDelayReached * @throws ServiceUnavailable */ public function check(RequestInterface $request, ResponseInterface $response): array { try { $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); + if (count($_COOKIE) > 0 && !$this->request->passesStrictCookieCheck() && $this->getShare()->getPassword() !== null) { + throw new PreconditionFailed('Strict cookie check failed'); + } + $auth = new HTTP\Auth\Basic( $this->realm, $request, @@ -105,7 +80,17 @@ class PublicAuth extends AbstractBasic { } return $this->checkToken(); - } catch (NotAuthenticated $e) { + } catch (NotAuthenticated|MaxDelayReached $e) { + $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); + throw $e; + } catch (PreconditionFailed $e) { + $response->setHeader( + 'Location', + $this->urlGenerator->linkToRoute( + 'files_sharing.share.showShare', + [ 'token' => $this->getToken() ], + ), + ); throw $e; } catch (\Exception $e) { $class = get_class($e); @@ -117,14 +102,13 @@ class PublicAuth extends AbstractBasic { /** * Extract token from request url - * @return string * @throws NotFound */ private function getToken(): string { $path = $this->request->getPathInfo() ?: ''; // ['', 'dav', 'files', 'token'] $splittedPath = explode('/', $path); - + if (count($splittedPath) < 4 || $splittedPath[3] === '') { throw new NotFound(); } @@ -134,7 +118,7 @@ class PublicAuth extends AbstractBasic { /** * Check token validity - * @return array + * * @throws NotFound * @throws NotAuthenticated */ @@ -182,15 +166,13 @@ class PublicAuth extends AbstractBasic { protected function validateUserPass($username, $password) { $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION); - $token = $this->getToken(); try { - $share = $this->shareManager->getShareByToken($token); + $share = $this->getShare(); } catch (ShareNotFound $e) { $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress()); return false; } - $this->share = $share; \OC_User::setIncognitoMode(true); // check if the share is password protected @@ -206,7 +188,7 @@ class PublicAuth extends AbstractBasic { } return true; } - + if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED) && $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) { return true; @@ -233,7 +215,13 @@ class PublicAuth extends AbstractBasic { } public function getShare(): IShare { - assert($this->share !== null); + $token = $this->getToken(); + + if ($this->share === null) { + $share = $this->shareManager->getShareByToken($token); + $this->share = $share; + } + return $this->share; } } diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php index b37325430e7..bbb378edc9b 100644 --- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php +++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php @@ -1,41 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved. - * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author scambra <sergio@entrecables.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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-FileCopyrightText: 2012 entreCables S.L. All rights reserved + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Connector\Sabre; +use OC\Files\View; use OCA\DAV\Upload\FutureFile; use OCA\DAV\Upload\UploadFolder; use OCP\Files\StorageNotAvailableException; use Sabre\DAV\Exception\InsufficientStorage; use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\DAV\INode; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; /** * This plugin check user quota and deny creating files when they exceeds the quota. @@ -45,9 +26,6 @@ use Sabre\DAV\INode; * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { - /** @var \OC\Files\View */ - private $view; - /** * Reference to main server object * @@ -56,10 +34,11 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { private $server; /** - * @param \OC\Files\View $view + * @param View $view */ - public function __construct($view) { - $this->view = $view; + public function __construct( + private $view, + ) { } /** @@ -78,6 +57,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { $server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10); $server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10); + $server->on('method:MKCOL', [$this, 'onCreateCollection'], 30); $server->on('beforeMove', [$this, 'beforeMove'], 10); $server->on('beforeCopy', [$this, 'beforeCopy'], 10); } @@ -112,6 +92,31 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { } /** + * Check quota before creating directory + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + * @throws InsufficientStorage + * @throws \Sabre\DAV\Exception\Forbidden + */ + public function onCreateCollection(RequestInterface $request, ResponseInterface $response): bool { + try { + $destinationPath = $this->server->calculateUri($request->getUrl()); + $quotaPath = $this->getPathForDestination($destinationPath); + } catch (\Exception $e) { + return true; + } + if ($quotaPath) { + // MKCOL does not have a Content-Length header, so we can use + // a fixed value for the quota check. + return $this->checkQuota($quotaPath, 4096, true); + } + + return true; + } + + /** * Check quota before writing content * * @param string $uri target file URI @@ -197,7 +202,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { * @throws InsufficientStorage * @return bool */ - public function checkQuota(string $path, $length = null) { + public function checkQuota(string $path, $length = null, $isDir = false) { if ($length === null) { $length = $this->getLength(); } @@ -209,26 +214,15 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { } $req = $this->server->httpRequest; - // If LEGACY chunked upload - if ($req->getHeader('OC-Chunked')) { - $info = \OC_FileChunking::decodeName($newName); - $chunkHandler = $this->getFileChunking($info); - // subtract the already uploaded size to see whether - // there is still enough space for the remaining chunks - $length -= $chunkHandler->getCurrentSize(); - // use target file name for free space check in case of shared files - $path = rtrim($parentPath, '/') . '/' . $info['name']; - } - // Strip any duplicate slashes $path = str_replace('//', '/', $path); $freeSpace = $this->getFreeSpace($path); if ($freeSpace >= 0 && $length > $freeSpace) { - // If LEGACY chunked upload, clean up - if (isset($chunkHandler)) { - $chunkHandler->cleanup(); + if ($isDir) { + throw new InsufficientStorage("Insufficient space in $path. $freeSpace available. Cannot create directory"); } + throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available"); } } @@ -236,11 +230,6 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin { return true; } - public function getFileChunking($info) { - // FIXME: need a factory for better mocking support - return new \OC_FileChunking($info); - } - public function getLength() { $req = $this->server->httpRequest; $length = $req->getHeader('X-Expected-Entity-Length'); diff --git a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php index df82db59f1d..5484bab9237 100644 --- a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php +++ b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php @@ -2,23 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; @@ -28,11 +13,9 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class RequestIdHeaderPlugin extends \Sabre\DAV\ServerPlugin { - /** @var IRequest */ - private $request; - - public function __construct(IRequest $request) { - $this->request = $request; + public function __construct( + private IRequest $request, + ) { } public function initialize(\Sabre\DAV\Server $server) { diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php index 6cf6fa954c8..dda9c29b763 100644 --- a/apps/dav/lib/Connector/Sabre/Server.php +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -1,30 +1,20 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author scolebrook <scolebrook@mac.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; +use OC\DB\Connection; +use Override; +use Sabre\DAV\Exception; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Version; +use TypeError; + /** * Class \OCA\DAV\Connector\Sabre\Server * @@ -36,6 +26,14 @@ class Server extends \Sabre\DAV\Server { /** @var CachingTree $tree */ /** + * Tracks queries done by plugins. + * @var array<int, array<string, array{nodes:int, queries:int}>> + */ + private array $pluginQueries = []; + + public bool $debugEnabled = false; + + /** * @see \Sabre\DAV\Server */ public function __construct($treeOrNode = null) { @@ -43,4 +41,190 @@ class Server extends \Sabre\DAV\Server { self::$exposeVersion = false; $this->enablePropfindDepthInfinity = true; } + + #[Override] + public function once( + string $eventName, + callable $callBack, + int $priority = 100, + ): void { + $this->debugEnabled ? $this->monitorPropfindQueries( + parent::once(...), + ...func_get_args(), + ) : parent::once(...func_get_args()); + } + + #[Override] + public function on( + string $eventName, + callable $callBack, + int $priority = 100, + ): void { + $this->debugEnabled ? $this->monitorPropfindQueries( + parent::on(...), + ...func_get_args(), + ) : parent::on(...func_get_args()); + } + + /** + * Wraps the handler $callBack into a query-monitoring function and calls + * $parentFn to register it. + */ + private function monitorPropfindQueries( + callable $parentFn, + string $eventName, + callable $callBack, + int $priority = 100, + ): void { + if ($eventName !== 'propFind') { + $parentFn($eventName, $callBack, $priority); + return; + } + + $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown'; + $callback = $this->getMonitoredCallback($callBack, $pluginName); + + $parentFn($eventName, $callback, $priority); + } + + /** + * Returns a callable that wraps $callBack with code that monitors and + * records queries per plugin. + */ + private function getMonitoredCallback( + callable $callBack, + string $pluginName, + ): callable { + return function (PropFind $propFind, INode $node) use ( + $callBack, + $pluginName, + ) { + $connection = \OCP\Server::get(Connection::class); + $queriesBefore = $connection->getStats()['executed']; + $result = $callBack($propFind, $node); + $queriesAfter = $connection->getStats()['executed']; + $this->trackPluginQueries( + $pluginName, + $queriesAfter - $queriesBefore, + $propFind->getDepth() + ); + + return $result; + }; + } + + /** + * Tracks the queries executed by a specific plugin. + */ + private function trackPluginQueries( + string $pluginName, + int $queriesExecuted, + int $depth, + ): void { + // report only nodes which cause queries to the DB + if ($queriesExecuted === 0) { + return; + } + + $this->pluginQueries[$depth][$pluginName]['nodes'] + = ($this->pluginQueries[$depth][$pluginName]['nodes'] ?? 0) + 1; + + $this->pluginQueries[$depth][$pluginName]['queries'] + = ($this->pluginQueries[$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted; + } + + /** + * + * @return void + */ + public function start() { + try { + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an + // origin, we must make sure we send back HTTP/1.0 if this was + // requested. + // This is mainly because nginx doesn't support Chunked Transfer + // Encoding, and this forces the webserver SabreDAV is running on, + // to buffer entire responses to calculate Content-Length. + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); + + // Setting the base url + $this->httpRequest->setBaseUrl($this->getBaseUri()); + $this->invokeMethod($this->httpRequest, $this->httpResponse); + } catch (\Throwable $e) { + try { + $this->emit('exception', [$e]); + } catch (\Exception) { + } + + if ($e instanceof TypeError) { + /* + * The TypeError includes the file path where the error occurred, + * potentially revealing the installation directory. + */ + $e = new TypeError('A type error occurred. For more details, please refer to the logs, which provide additional context about the type error.'); + } + + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', self::NS_SABREDAV); + $DOM->appendChild($error); + + $h = function ($v) { + return htmlspecialchars((string)$v, ENT_NOQUOTES, 'UTF-8'); + }; + + if (self::$exposeVersion) { + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); + } + + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); + if ($this->debugExceptions) { + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); + } + + if ($this->debugExceptions) { + $previous = $e; + while ($previous = $previous->getPrevious()) { + $xPrevious = $DOM->createElement('s:previous-exception'); + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); + $error->appendChild($xPrevious); + } + } + + if ($e instanceof Exception) { + $httpCode = $e->getHTTPCode(); + $e->serialize($this, $error); + $headers = $e->getHTTPHeaders($this); + } else { + $httpCode = 500; + $headers = []; + } + $headers['Content-Type'] = 'application/xml; charset=utf-8'; + + $this->httpResponse->setStatus($httpCode); + $this->httpResponse->setHeaders($headers); + $this->httpResponse->setBody($DOM->saveXML()); + $this->sapi->sendResponse($this->httpResponse); + } + } + + /** + * Returns queries executed by registered plugins. + * + * @return array<int, array<string, array{nodes:int, queries:int}>> + */ + public function getPluginQueries(): array { + return $this->pluginQueries; + } } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 758951c42ff..a6a27057177 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -1,114 +1,111 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; +use OC\Files\View; +use OC\KnownUser\KnownUserService; use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCA\DAV\Files\Sharing\RootCollection; +use OCA\DAV\Upload\CleanupService; +use OCA\Theming\ThemingDefaults; +use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; +use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Folder; +use OCP\Files\IFilenameValidator; +use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IPreview; use OCP\IRequest; use OCP\ITagManager; +use OCP\IUserManager; use OCP\IUserSession; use OCP\SabrePluginEvent; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use Psr\Log\LoggerInterface; use Sabre\DAV\Auth\Plugin; +use Sabre\DAV\SimpleCollection; class ServerFactory { - private IConfig $config; - private LoggerInterface $logger; - private IDBConnection $databaseConnection; - private IUserSession $userSession; - private IMountManager $mountManager; - private ITagManager $tagManager; - private IRequest $request; - private IPreview $previewManager; - private IEventDispatcher $eventDispatcher; - private IL10N $l10n; public function __construct( - IConfig $config, - LoggerInterface $logger, - IDBConnection $databaseConnection, - IUserSession $userSession, - IMountManager $mountManager, - ITagManager $tagManager, - IRequest $request, - IPreview $previewManager, - IEventDispatcher $eventDispatcher, - IL10N $l10n + private IConfig $config, + private LoggerInterface $logger, + private IDBConnection $databaseConnection, + private IUserSession $userSession, + private IMountManager $mountManager, + private ITagManager $tagManager, + private IRequest $request, + private IPreview $previewManager, + private IEventDispatcher $eventDispatcher, + private IL10N $l10n, ) { - $this->config = $config; - $this->logger = $logger; - $this->databaseConnection = $databaseConnection; - $this->userSession = $userSession; - $this->mountManager = $mountManager; - $this->tagManager = $tagManager; - $this->request = $request; - $this->previewManager = $previewManager; - $this->eventDispatcher = $eventDispatcher; - $this->l10n = $l10n; } /** * @param callable $viewCallBack callback that should return the view for the dav endpoint */ - public function createServer(string $baseUri, + public function createServer( + bool $isPublicShare, + string $baseUri, string $requestUri, Plugin $authPlugin, - callable $viewCallBack): Server { + callable $viewCallBack, + ): Server { + $debugEnabled = $this->config->getSystemValue('debug', false); // Fire up server - $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree(); - $server = new \OCA\DAV\Connector\Sabre\Server($objectTree); + if ($isPublicShare) { + $rootCollection = new SimpleCollection('root'); + $tree = new CachingTree($rootCollection); + } else { + $rootCollection = null; + $tree = new ObjectTree(); + } + $server = new Server($tree); // Set URL explicitly due to reverse-proxy situations $server->httpRequest->setUrl($requestUri); $server->setBaseUri($baseUri); // Load plugins - $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin()); + $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n)); + $server->addPlugin(new BlockLegacyClientPlugin( + $this->config, + \OCP\Server::get(ThemingDefaults::class), + )); + $server->addPlugin(new AnonymousOptionsPlugin()); $server->addPlugin($authPlugin); + if ($debugEnabled) { + $server->debugEnabled = $debugEnabled; + $server->addPlugin(new PropFindMonitorPlugin()); + } // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / - $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin()); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + $server->addPlugin(new DummyGetResponsePlugin()); + $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); + $server->addPlugin(new LockPlugin()); - $server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class))); + $server->addPlugin(new RequestIdHeaderPlugin($this->request)); + + $server->addPlugin(new ZipFolderPlugin( + $tree, + $this->logger, + $this->eventDispatcher, + )); // Some WebDAV clients do require Class 2 WebDAV support (locking), since // we do not provide locking we emulate it using a fake locking plugin. @@ -117,7 +114,7 @@ class ServerFactory { '/OneNote/', '/Microsoft-WebDAV-MiniRedir/', ])) { - $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin()); + $server->addPlugin(new FakeLockerPlugin()); } if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { @@ -125,11 +122,12 @@ class ServerFactory { } // wait with registering these until auth is handled and the filesystem is setup - $server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack) { + $server->on('beforeMethod:*', function () use ($server, $tree, + $viewCallBack, $isPublicShare, $rootCollection, $debugEnabled): void { // ensure the skeleton is copied $userFolder = \OC::$server->getUserFolder(); - /** @var \OC\Files\View $view */ + /** @var View $view */ $view = $viewCallBack($server); if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) { $rootInfo = $userFolder; @@ -139,25 +137,61 @@ class ServerFactory { // Create Nextcloud Dir if ($rootInfo->getType() === 'dir') { - $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); + $root = new Directory($view, $rootInfo, $tree); + } else { + $root = new File($view, $rootInfo); + } + + if ($isPublicShare) { + $userPrincipalBackend = new Principal( + \OCP\Server::get(IUserManager::class), + \OCP\Server::get(IGroupManager::class), + \OCP\Server::get(IAccountManager::class), + \OCP\Server::get(\OCP\Share\IManager::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IAppManager::class), + \OCP\Server::get(ProxyMapper::class), + \OCP\Server::get(KnownUserService::class), + \OCP\Server::get(IConfig::class), + \OC::$server->getL10NFactory(), + ); + + // Mount the share collection at /public.php/dav/shares/<share token> + $rootCollection->addChild(new RootCollection( + $root, + $userPrincipalBackend, + 'principals/shares', + )); + + // Mount the upload collection at /public.php/dav/uploads/<share token> + $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection( + $userPrincipalBackend, + 'principals/shares', + \OCP\Server::get(CleanupService::class), + \OCP\Server::get(IRootFolder::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(\OCP\Share\IManager::class), + )); } else { - $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo); + /** @var ObjectTree $tree */ + $tree->init($root, $view, $this->mountManager); } - $objectTree->init($root, $view, $this->mountManager); $server->addPlugin( - new \OCA\DAV\Connector\Sabre\FilesPlugin( - $objectTree, + new FilesPlugin( + $tree, $this->config, $this->request, $this->previewManager, $this->userSession, + \OCP\Server::get(IFilenameValidator::class), + \OCP\Server::get(IAccountManager::class), false, - !$this->config->getSystemValue('debug', false) + !$debugEnabled ) ); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin()); + $server->addPlugin(new QuotaPlugin($view)); + $server->addPlugin(new ChecksumUpdatePlugin()); // Allow view-only plugin for webdav requests $server->addPlugin(new ViewOnlyPlugin( @@ -165,45 +199,46 @@ class ServerFactory { )); if ($this->userSession->isLoggedIn()) { - $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( - $objectTree, + $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession)); + $server->addPlugin(new SharesPlugin( + $tree, $this->userSession, $userFolder, - \OC::$server->getShareManager() + \OCP\Server::get(\OCP\Share\IManager::class) )); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession)); - $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin( - $objectTree, + $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession)); + $server->addPlugin(new FilesReportPlugin( + $tree, $view, - \OC::$server->getSystemTagManager(), - \OC::$server->getSystemTagObjectMapper(), - \OC::$server->getTagManager(), + \OCP\Server::get(ISystemTagManager::class), + \OCP\Server::get(ISystemTagObjectMapper::class), + \OCP\Server::get(ITagManager::class), $this->userSession, - \OC::$server->getGroupManager(), + \OCP\Server::get(IGroupManager::class), $userFolder, - \OC::$server->getAppManager() + \OCP\Server::get(IAppManager::class) )); // custom properties plugin must be the last one $server->addPlugin( new \Sabre\DAV\PropertyStorage\Plugin( - new \OCA\DAV\DAV\CustomPropertiesBackend( + new CustomPropertiesBackend( $server, - $objectTree, + $tree, $this->databaseConnection, - $this->userSession->getUser() + $this->userSession->getUser(), + \OCP\Server::get(DefaultCalendarValidator::class), ) ) ); } - $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin()); + $server->addPlugin(new CopyEtagHeaderPlugin()); // Load dav plugins from apps $event = new SabrePluginEvent($server); $this->eventDispatcher->dispatchTyped($event); $pluginManager = new PluginManager( \OC::$server, - \OC::$server->getAppManager() + \OCP\Server::get(IAppManager::class) ); foreach ($pluginManager->getAppPlugins() as $appPlugin) { $server->addPlugin($appPlugin); diff --git a/apps/dav/lib/Connector/Sabre/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php index bacbdc99a73..0b66ed27576 100644 --- a/apps/dav/lib/Connector/Sabre/ShareTypeList.php +++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -35,17 +20,14 @@ class ShareTypeList implements Element { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; /** - * Share types - * - * @var int[] - */ - private $shareTypes; - - /** * @param int[] $shareTypes */ - public function __construct($shareTypes) { - $this->shareTypes = $shareTypes; + public function __construct( + /** + * Share types + */ + private $shareTypes, + ) { } /** diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php index e43f552a8cc..909c29fc24b 100644 --- a/apps/dav/lib/Connector/Sabre/ShareeList.php +++ b/apps/dav/lib/Connector/Sabre/ShareeList.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Connector\Sabre; @@ -37,11 +18,10 @@ use Sabre\Xml\XmlSerializable; class ShareeList implements XmlSerializable { public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; - /** @var IShare[] */ - private $shares; - - public function __construct(array $shares) { - $this->shares = $shares; + public function __construct( + /** @var IShare[] */ + private array $shares, + ) { } /** diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index 4cda346af01..f49e85333f3 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -1,33 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; +use OC\Share20\Exception\BackendError; use OCA\DAV\Connector\Sabre\Node as DavNode; use OCP\Files\Folder; use OCP\Files\Node; @@ -54,24 +34,19 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { * @var \Sabre\DAV\Server */ private $server; - private IManager $shareManager; - private Tree $tree; private string $userId; - private Folder $userFolder; + /** @var IShare[][] */ private array $cachedShares = []; /** @var string[] */ private array $cachedFolders = []; public function __construct( - Tree $tree, - IUserSession $userSession, - Folder $userFolder, - IManager $shareManager + private Tree $tree, + private IUserSession $userSession, + private Folder $userFolder, + private IManager $shareManager, ) { - $this->tree = $tree; - $this->shareManager = $shareManager; - $this->userFolder = $userFolder; $this->userId = $userSession->getUser()->getUID(); } @@ -112,18 +87,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { IShare::TYPE_DECK, IShare::TYPE_SCIENCEMESH, ]; + foreach ($requestedShareTypes as $requestedShareType) { - $shares = $this->shareManager->getSharesBy( + $result = array_merge($result, $this->shareManager->getSharesBy( $this->userId, $requestedShareType, $node, false, -1 - ); - foreach ($shares as $share) { - $result[] = $share; + )); + + // Also check for shares where the user is the recipient + try { + $result = array_merge($result, $this->shareManager->getSharedWith( + $this->userId, + $requestedShareType, + $node, + -1 + )); + } catch (BackendError $e) { + // ignore } } + return $result; } @@ -145,27 +131,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { */ private function getShares(DavNode $sabreNode): array { if (isset($this->cachedShares[$sabreNode->getId()])) { - $shares = $this->cachedShares[$sabreNode->getId()]; - } else { - [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath()); - if ($parentPath === '') { - $parentPath = '/'; - } - // if we already cached the folder this file is in we know there are no shares for this file - if (array_search($parentPath, $this->cachedFolders) === false) { - try { - $node = $sabreNode->getNode(); - } catch (NotFoundException $e) { - return []; - } - $shares = $this->getShare($node); - $this->cachedShares[$sabreNode->getId()] = $shares; - } else { + return $this->cachedShares[$sabreNode->getId()]; + } + + [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath()); + if ($parentPath === '') { + $parentPath = '/'; + } + + // if we already cached the folder containing this file + // then we already know there are no shares here. + if (array_search($parentPath, $this->cachedFolders) === false) { + try { + $node = $sabreNode->getNode(); + } catch (NotFoundException $e) { return []; } + + $shares = $this->getShare($node); + $this->cachedShares[$sabreNode->getId()] = $shares; + return $shares; } - return $shares; + return []; } /** @@ -176,18 +164,20 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { */ public function handleGetProperties( PropFind $propFind, - \Sabre\DAV\INode $sabreNode + \Sabre\DAV\INode $sabreNode, ) { if (!($sabreNode instanceof DavNode)) { return; } - // need prefetch ? + // If the node is a directory and we are requesting share types or sharees + // then we get all the shares in the folder and cache them. + // This is more performant than iterating each files afterwards. if ($sabreNode instanceof Directory && $propFind->getDepth() !== 0 && ( - !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) || - !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) + !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) + || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) ) ) { $folderNode = $sabreNode->getNode(); diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php index 86006cd3404..9a5cd0d51cf 100644 --- a/apps/dav/lib/Connector/Sabre/TagList.php +++ b/apps/dav/lib/Connector/Sabre/TagList.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -36,17 +20,14 @@ class TagList implements Element { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; /** - * tags - * - * @var array - */ - private $tags; - - /** * @param array $tags */ - public function __construct(array $tags) { - $this->tags = $tags; + public function __construct( + /** + * tags + */ + private array $tags, + ) { } /** diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php index dc2b6b0c339..25c1633df36 100644 --- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -1,31 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright 2014 Vincent Petry <pvince81@owncloud.com> - * @copyright 2014 Vincent Petry <pvince81@owncloud.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Connector\Sabre; @@ -49,7 +27,10 @@ namespace OCA\DAV\Connector\Sabre; * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ - +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ITagManager; +use OCP\ITags; +use OCP\IUserSession; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -69,12 +50,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { private $server; /** - * @var \OCP\ITagManager - */ - private $tagManager; - - /** - * @var \OCP\ITags + * @var ITags */ private $tagger; @@ -87,17 +63,15 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { private $cachedTags; /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** * @param \Sabre\DAV\Tree $tree tree - * @param \OCP\ITagManager $tagManager tag manager + * @param ITagManager $tagManager tag manager */ - public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) { - $this->tree = $tree; - $this->tagManager = $tagManager; + public function __construct( + private \Sabre\DAV\Tree $tree, + private ITagManager $tagManager, + private IEventDispatcher $eventDispatcher, + private IUserSession $userSession, + ) { $this->tagger = null; $this->cachedTags = []; } @@ -120,12 +94,13 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { $this->server = $server; $this->server->on('propFind', [$this, 'handleGetProperties']); $this->server->on('propPatch', [$this, 'handleUpdateProperties']); + $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']); } /** * Returns the tagger * - * @return \OCP\ITags tagger + * @return ITags tagger */ private function getTagger() { if (!$this->tagger) { @@ -139,7 +114,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { * * @param integer $fileId file id * @return array list($tags, $favorite) with $tags as tag array - * and $favorite is a boolean whether the file was favorited + * and $favorite is a boolean whether the file was favorited */ private function getTagsAndFav($fileId) { $isFav = false; @@ -176,6 +151,24 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { } /** + * Prefetches tags for a list of file IDs and caches the results + * + * @param array $fileIds List of file IDs to prefetch tags for + * @return void + */ + private function prefetchTagsForFileIds(array $fileIds) { + $tags = $this->getTagger()->getTagsForObjects($fileIds); + if ($tags === false) { + // the tags API returns false on error... + $tags = []; + } + + foreach ($fileIds as $fileId) { + $this->cachedTags[$fileId] = $tags[$fileId] ?? []; + } + } + + /** * Updates the tags of the given file id * * @param int $fileId @@ -211,36 +204,25 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { */ public function handleGetProperties( PropFind $propFind, - \Sabre\DAV\INode $node + \Sabre\DAV\INode $node, ) { - if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) { + if (!($node instanceof Node)) { return; } // need prefetch ? - if ($node instanceof \OCA\DAV\Connector\Sabre\Directory + if ($node instanceof Directory && $propFind->getDepth() !== 0 && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) )) { // note: pre-fetching only supported for depth <= 1 $folderContent = $node->getChildren(); - $fileIds[] = (int)$node->getId(); + $fileIds = [(int)$node->getId()]; foreach ($folderContent as $info) { $fileIds[] = (int)$info->getId(); } - $tags = $this->getTagger()->getTagsForObjects($fileIds); - if ($tags === false) { - // the tags API returns false on error... - $tags = []; - } - - $this->cachedTags = $this->cachedTags + $tags; - $emptyFileIds = array_diff($fileIds, array_keys($tags)); - // also cache the ones that were not found - foreach ($emptyFileIds as $fileId) { - $this->cachedTags[$fileId] = []; - } + $this->prefetchTagsForFileIds($fileIds); } $isFav = null; @@ -272,7 +254,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { */ public function handleUpdateProperties($path, PropPatch $propPatch) { $node = $this->tree->getNodeForPath($path); - if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) { + if (!($node instanceof Node)) { return; } @@ -281,7 +263,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { return true; }); - $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) { + $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) { if ((int)$favState === 1 || $favState === 'true') { $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE); } else { @@ -296,4 +278,14 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { return 200; }); } + + public function handlePreloadProperties(array $nodes, array $requestProperties): void { + if ( + !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true) + && !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true) + ) { + return; + } + $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes)); + } } diff --git a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php new file mode 100644 index 00000000000..f198519b454 --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php @@ -0,0 +1,193 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Connector\Sabre; + +use OC\Streamer; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\File as NcFile; +use OCP\Files\Folder as NcFolder; +use OCP\Files\Node as NcNode; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Tree; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +/** + * This plugin allows to download folders accessed by GET HTTP requests on DAV. + * The WebDAV standard explicitly say that GET is not covered and should return what ever the application thinks would be a good representation. + * + * When a collection is accessed using GET, this will provide the content as a archive. + * The type can be set by the `Accept` header (MIME type of zip or tar), or as browser fallback using a `accept` GET parameter. + * It is also possible to only include some child nodes (from the collection it self) by providing a `filter` GET parameter or `X-NC-Files` custom header. + */ +class ZipFolderPlugin extends ServerPlugin { + + /** + * Reference to main server object + */ + private ?Server $server = null; + + public function __construct( + private Tree $tree, + private LoggerInterface $logger, + private IEventDispatcher $eventDispatcher, + ) { + } + + /** + * This initializes the plugin. + * + * This function is called by \Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + */ + public function initialize(Server $server): void { + $this->server = $server; + $this->server->on('method:GET', $this->handleDownload(...), 100); + // low priority to give any other afterMethod:* a chance to fire before we cancel everything + $this->server->on('afterMethod:GET', $this->afterDownload(...), 999); + } + + /** + * Adding a node to the archive streamer. + * This will recursively add new nodes to the stream if the node is a directory. + */ + protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void { + // Remove the root path from the filename to make it relative to the requested folder + $filename = str_replace($rootPath, '', $node->getPath()); + + $mtime = $node->getMTime(); + if ($node instanceof NcFile) { + $resource = $node->fopen('rb'); + if ($resource === false) { + $this->logger->info('Cannot read file for zip stream', ['filePath' => $node->getPath()]); + throw new \Sabre\DAV\Exception\ServiceUnavailable('Requested file can currently not be accessed.'); + } + $streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime); + } elseif ($node instanceof NcFolder) { + $streamer->addEmptyDir($filename, $mtime); + $content = $node->getDirectoryListing(); + foreach ($content as $subNode) { + $this->streamNode($streamer, $subNode, $rootPath); + } + } + } + + /** + * Download a folder as an archive. + * It is possible to filter / limit the files that should be downloaded, + * either by passing (multiple) `X-NC-Files: the-file` headers + * or by setting a `files=JSON_ARRAY_OF_FILES` URL query. + * + * @return false|null + */ + public function handleDownload(Request $request, Response $response): ?bool { + $node = $this->tree->getNodeForPath($request->getPath()); + if (!($node instanceof Directory)) { + // only handle directories + return null; + } + + $query = $request->getQueryParameters(); + + // Get accept header - or if set overwrite with accept GET-param + $accept = $request->getHeaderAsArray('Accept'); + $acceptParam = $query['accept'] ?? ''; + if ($acceptParam !== '') { + $accept = array_map(fn (string $name) => strtolower(trim($name)), explode(',', $acceptParam)); + } + $zipRequest = !empty(array_intersect(['application/zip', 'zip'], $accept)); + $tarRequest = !empty(array_intersect(['application/x-tar', 'tar'], $accept)); + if (!$zipRequest && !$tarRequest) { + // does not accept zip or tar stream + return null; + } + + $files = $request->getHeaderAsArray('X-NC-Files'); + $filesParam = $query['files'] ?? ''; + // The preferred way would be headers, but this is not possible for simple browser requests ("links") + // so we also need to support GET parameters + if ($filesParam !== '') { + $files = json_decode($filesParam); + if (!is_array($files)) { + $files = [$files]; + } + + foreach ($files as $file) { + if (!is_string($file)) { + // we log this as this means either we - or an app - have a bug somewhere or a user is trying invalid things + $this->logger->notice('Invalid files filter parameter for ZipFolderPlugin', ['filter' => $filesParam]); + // no valid parameter so continue with Sabre behavior + return null; + } + } + } + + $folder = $node->getNode(); + $event = new BeforeZipCreatedEvent($folder, $files); + $this->eventDispatcher->dispatchTyped($event); + if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) { + $errorMessage = $event->getErrorMessage(); + if ($errorMessage === null) { + // Not allowed to download but also no explaining error + // so we abort the ZIP creation and fall back to Sabre default behavior. + return null; + } + // Downloading was denied by an app + throw new Forbidden($errorMessage); + } + + $content = empty($files) ? $folder->getDirectoryListing() : []; + foreach ($files as $path) { + $child = $node->getChild($path); + assert($child instanceof Node); + $content[] = $child->getNode(); + } + + $archiveName = 'download'; + $rootPath = $folder->getPath(); + if (empty($files)) { + // We download the full folder so keep it in the tree + $rootPath = dirname($folder->getPath()); + // Full folder is loaded to rename the archive to the folder name + $archiveName = $folder->getName(); + } + $streamer = new Streamer($tarRequest, -1, count($content)); + $streamer->sendHeaders($archiveName); + // For full folder downloads we also add the folder itself to the archive + if (empty($files)) { + $streamer->addEmptyDir($archiveName); + } + foreach ($content as $node) { + $this->streamNode($streamer, $node, $rootPath); + } + $streamer->finalize(); + return false; + } + + /** + * Tell sabre/dav not to trigger it's own response sending logic as the handleDownload will have already send the response + * + * @return false|null + */ + public function afterDownload(Request $request, Response $response): ?bool { + $node = $this->tree->getNodeForPath($request->getPath()); + if (!($node instanceof Directory)) { + // only handle directories + return null; + } else { + return false; + } + } +} diff --git a/apps/dav/lib/Controller/BirthdayCalendarController.php b/apps/dav/lib/Controller/BirthdayCalendarController.php index 5df13cefe97..f6bfb229a9c 100644 --- a/apps/dav/lib/Controller/BirthdayCalendarController.php +++ b/apps/dav/lib/Controller/BirthdayCalendarController.php @@ -1,31 +1,16 @@ <?php + /** - * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Controller; use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob; use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Settings\CalDAVSettings; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\BackgroundJob\IJobList; @@ -38,31 +23,6 @@ use OCP\IUserManager; class BirthdayCalendarController extends Controller { /** - * @var IDBConnection - */ - protected $db; - - /** - * @var IConfig - */ - protected $config; - - /** - * @var IUserManager - */ - protected $userManager; - - /** - * @var CalDavBackend - */ - protected $caldavBackend; - - /** - * @var IJobList - */ - protected $jobList; - - /** * BirthdayCalendar constructor. * * @param string $appName @@ -71,30 +31,29 @@ class BirthdayCalendarController extends Controller { * @param IConfig $config * @param IJobList $jobList * @param IUserManager $userManager - * @param CalDavBackend $calDavBackend + * @param CalDavBackend $caldavBackend */ - public function __construct($appName, IRequest $request, - IDBConnection $db, IConfig $config, - IJobList $jobList, - IUserManager $userManager, - CalDavBackend $calDavBackend) { + public function __construct( + $appName, + IRequest $request, + protected IDBConnection $db, + protected IConfig $config, + protected IJobList $jobList, + protected IUserManager $userManager, + protected CalDavBackend $caldavBackend, + ) { parent::__construct($appName, $request); - $this->db = $db; - $this->config = $config; - $this->userManager = $userManager; - $this->jobList = $jobList; - $this->caldavBackend = $calDavBackend; } /** * @return Response - * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings) */ + #[AuthorizedAdminSetting(settings: CalDAVSettings::class)] public function enable() { $this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'yes'); // add background job for each user - $this->userManager->callForSeenUsers(function (IUser $user) { + $this->userManager->callForSeenUsers(function (IUser $user): void { $this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [ 'userId' => $user->getUID(), ]); @@ -105,8 +64,8 @@ class BirthdayCalendarController extends Controller { /** * @return Response - * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings) */ + #[AuthorizedAdminSetting(settings: CalDAVSettings::class)] public function disable() { $this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'no'); diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php index 2d4522c8d37..ea209168123 100644 --- a/apps/dav/lib/Controller/DirectController.php +++ b/apps/dav/lib/Controller/DirectController.php @@ -3,33 +3,15 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Iscle <albertiscle9@gmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Controller; use OCA\DAV\Db\Direct; use OCA\DAV\Db\DirectMapper; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -46,50 +28,21 @@ use OCP\Security\ISecureRandom; class DirectController extends OCSController { - /** @var IRootFolder */ - private $rootFolder; - - /** @var string */ - private $userId; - - /** @var DirectMapper */ - private $mapper; - - /** @var ISecureRandom */ - private $random; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var IEventDispatcher */ - private $eventDispatcher; - - public function __construct(string $appName, + public function __construct( + string $appName, IRequest $request, - IRootFolder $rootFolder, - string $userId, - DirectMapper $mapper, - ISecureRandom $random, - ITimeFactory $timeFactory, - IURLGenerator $urlGenerator, - IEventDispatcher $eventDispatcher) { + private IRootFolder $rootFolder, + private string $userId, + private DirectMapper $mapper, + private ISecureRandom $random, + private ITimeFactory $timeFactory, + private IURLGenerator $urlGenerator, + private IEventDispatcher $eventDispatcher, + ) { parent::__construct($appName, $request); - - $this->rootFolder = $rootFolder; - $this->userId = $userId; - $this->mapper = $mapper; - $this->random = $random; - $this->timeFactory = $timeFactory; - $this->urlGenerator = $urlGenerator; - $this->eventDispatcher = $eventDispatcher; } /** - * @NoAdminRequired - * * Get a direct link to a file * * @param int $fileId ID of the file @@ -101,6 +54,7 @@ class DirectController extends OCSController { * * 200: Direct link returned */ + #[NoAdminRequired] public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse { $userFolder = $this->rootFolder->getUserFolder($this->userId); @@ -136,7 +90,7 @@ class DirectController extends OCSController { $this->mapper->insert($direct); - $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/'.$token); + $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/' . $token); return new DataResponse([ 'url' => $url, diff --git a/apps/dav/lib/Controller/ExampleContentController.php b/apps/dav/lib/Controller/ExampleContentController.php new file mode 100644 index 00000000000..e20ee4b7f49 --- /dev/null +++ b/apps/dav/lib/Controller/ExampleContentController.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Controller; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\Service\ExampleContactService; +use OCA\DAV\Service\ExampleEventService; +use OCP\AppFramework\ApiController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\FrontpageRoute; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\DataDownloadResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class ExampleContentController extends ApiController { + public function __construct( + IRequest $request, + private readonly LoggerInterface $logger, + private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, + ) { + parent::__construct(Application::APP_ID, $request); + } + + #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/config')] + public function setEnableDefaultContact(bool $allow): JSONResponse { + if ($allow && !$this->exampleContactService->defaultContactExists()) { + try { + $this->exampleContactService->setCard(); + } catch (\Exception $e) { + $this->logger->error('Could not create default contact', ['exception' => $e]); + return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + $this->exampleContactService->setDefaultContactEnabled($allow); + return new JSONResponse([], Http::STATUS_OK); + } + + #[NoCSRFRequired] + #[FrontpageRoute(verb: 'GET', url: '/api/defaultcontact/contact')] + public function getDefaultContact(): DataDownloadResponse { + $cardData = $this->exampleContactService->getCard() + ?? file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); + return new DataDownloadResponse($cardData, 'example_contact.vcf', 'text/vcard'); + } + + #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/contact')] + public function setDefaultContact(?string $contactData = null) { + if (!$this->exampleContactService->isDefaultContactEnabled()) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $this->exampleContactService->setCard($contactData); + return new JSONResponse([], Http::STATUS_OK); + } + + #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')] + public function setCreateExampleEvent(bool $enable): JSONResponse { + $this->exampleEventService->setCreateExampleEvent($enable); + return new JsonResponse([]); + } + + #[FrontpageRoute(verb: 'GET', url: '/api/exampleEvent/event')] + #[NoCSRFRequired] + public function downloadExampleEvent(): DataDownloadResponse { + $exampleEvent = $this->exampleEventService->getExampleEvent(); + return new DataDownloadResponse( + $exampleEvent->getIcs(), + 'example_event.ics', + 'text/calendar', + ); + } + + #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/event')] + public function uploadExampleEvent(string $ics): JSONResponse { + if (!$this->exampleEventService->shouldCreateExampleEvent()) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + $this->exampleEventService->saveCustomExampleEvent($ics); + return new JsonResponse([]); + } + + #[FrontpageRoute(verb: 'DELETE', url: '/api/exampleEvent/event')] + public function deleteExampleEvent(): JSONResponse { + $this->exampleEventService->deleteCustomExampleEvent(); + return new JsonResponse([]); + } + +} diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php index 4a32966220f..19eb4097b45 100644 --- a/apps/dav/lib/Controller/InvitationResponseController.php +++ b/apps/dav/lib/Controller/InvitationResponseController.php @@ -3,34 +3,16 @@ declare(strict_types=1); /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Kate Döen <kate.doeen@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Controller; use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IDBConnection; @@ -41,15 +23,6 @@ use Sabre\VObject\Reader; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class InvitationResponseController extends Controller { - /** @var IDBConnection */ - private $db; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var InvitationResponseServer */ - private $responseServer; - /** * InvitationResponseController constructor. * @@ -59,25 +32,25 @@ class InvitationResponseController extends Controller { * @param ITimeFactory $timeFactory * @param InvitationResponseServer $responseServer */ - public function __construct(string $appName, IRequest $request, - IDBConnection $db, ITimeFactory $timeFactory, - InvitationResponseServer $responseServer) { + public function __construct( + string $appName, + IRequest $request, + private IDBConnection $db, + private ITimeFactory $timeFactory, + private InvitationResponseServer $responseServer, + ) { parent::__construct($appName, $request); - $this->db = $db; - $this->timeFactory = $timeFactory; - $this->responseServer = $responseServer; // Don't run `$server->exec()`, because we just need access to the // fully initialized schedule plugin, but we don't want Sabre/DAV // to actually handle and reply to the request } /** - * @PublicPage - * @NoCSRFRequired - * * @param string $token * @return TemplateResponse */ + #[PublicPage] + #[NoCSRFRequired] public function accept(string $token):TemplateResponse { $row = $this->getTokenInformation($token); if (!$row) { @@ -96,12 +69,11 @@ class InvitationResponseController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @param string $token * @return TemplateResponse */ + #[PublicPage] + #[NoCSRFRequired] public function decline(string $token):TemplateResponse { $row = $this->getTokenInformation($token); if (!$row) { @@ -121,12 +93,11 @@ class InvitationResponseController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @param string $token * @return TemplateResponse */ + #[PublicPage] + #[NoCSRFRequired] public function options(string $token):TemplateResponse { return new TemplateResponse($this->appName, 'schedule-response-options', [ 'token' => $token @@ -134,13 +105,12 @@ class InvitationResponseController extends Controller { } /** - * @PublicPage - * @NoCSRFRequired - * * @param string $token * * @return TemplateResponse */ + #[PublicPage] + #[NoCSRFRequired] public function processMoreOptionsResult(string $token):TemplateResponse { $partstat = $this->request->getParam('partStat'); @@ -178,7 +148,7 @@ class InvitationResponseController extends Controller { } $currentTime = $this->timeFactory->getTime(); - if (((int) $row['expiration']) < $currentTime) { + if (((int)$row['expiration']) < $currentTime) { return null; } diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php index 5ab99c34b79..d3516d092e8 100644 --- a/apps/dav/lib/Controller/OutOfOfficeController.php +++ b/apps/dav/lib/Controller/OutOfOfficeController.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Controller; @@ -38,6 +21,7 @@ use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; use OCP\User\IAvailabilityCoordinator; +use function mb_strlen; /** * @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions @@ -110,6 +94,8 @@ class OutOfOfficeController extends OCSController { 'lastDay' => $data->getLastDay(), 'status' => $data->getStatus(), 'message' => $data->getMessage(), + 'replacementUserId' => $data->getReplacementUserId(), + 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(), ]); } @@ -120,11 +106,13 @@ class OutOfOfficeController extends OCSController { * @param string $lastDay Last day of the absence in format `YYYY-MM-DD` * @param string $status Short text that is set as user status during the absence * @param string $message Longer multiline message that is shown to others during the absence - * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}> + * @param ?string $replacementUserId User id of the replacement user + * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'|'statusLength'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}> * * 200: Absence data - * 400: When the first day is not before the last day + * 400: When validation fails, e.g. data range error or the first day is not before the last day * 401: When the user is not logged in + * 404: When the replacementUserId was provided but replacement user was not found */ #[NoAdminRequired] public function setOutOfOffice( @@ -132,11 +120,23 @@ class OutOfOfficeController extends OCSController { string $lastDay, string $status, string $message, + ?string $replacementUserId, ): DataResponse { $user = $this->userSession?->getUser(); if ($user === null) { return new DataResponse(null, Http::STATUS_UNAUTHORIZED); } + if (mb_strlen($status) > 100) { + return new DataResponse(['error' => 'statusLength'], Http::STATUS_BAD_REQUEST); + } + + $replacementUser = null; + if ($replacementUserId !== null) { + $replacementUser = $this->userManager->get($replacementUserId); + if ($replacementUser === null) { + return new DataResponse(null, Http::STATUS_NOT_FOUND); + } + } $parsedFirstDay = new DateTimeImmutable($firstDay); $parsedLastDay = new DateTimeImmutable($lastDay); @@ -150,6 +150,8 @@ class OutOfOfficeController extends OCSController { $lastDay, $status, $message, + $replacementUserId, + $replacementUser?->getDisplayName() ); $this->coordinator->clearCache($user->getUID()); @@ -160,6 +162,8 @@ class OutOfOfficeController extends OCSController { 'lastDay' => $data->getLastDay(), 'status' => $data->getStatus(), 'message' => $data->getMessage(), + 'replacementUserId' => $data->getReplacementUserId(), + 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(), ]); } diff --git a/apps/dav/lib/Controller/UpcomingEventsController.php b/apps/dav/lib/Controller/UpcomingEventsController.php new file mode 100644 index 00000000000..a5d54f44754 --- /dev/null +++ b/apps/dav/lib/Controller/UpcomingEventsController.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Controller; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\UpcomingEvent; +use OCA\DAV\CalDAV\UpcomingEventsService; +use OCA\DAV\ResponseDefinitions; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +/** + * @psalm-import-type DAVUpcomingEvent from ResponseDefinitions + */ +class UpcomingEventsController extends OCSController { + public function __construct( + IRequest $request, + private ?string $userId, + private UpcomingEventsService $service, + ) { + parent::__construct(Application::APP_ID, $request); + } + + /** + * Get information about upcoming events + * + * @param string|null $location location/URL to filter by + * @return DataResponse<Http::STATUS_OK, array{events: list<DAVUpcomingEvent>}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}> + * + * 200: Upcoming events + * 401: When not authenticated + */ + #[NoAdminRequired] + public function getEvents(?string $location = null): DataResponse { + if ($this->userId === null) { + return new DataResponse(null, Http::STATUS_UNAUTHORIZED); + } + + return new DataResponse([ + 'events' => array_values(array_map(fn (UpcomingEvent $e) => $e->jsonSerialize(), $this->service->getEvents( + $this->userId, + $location, + ))), + ]); + } + +} diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index 48872048ea8..f9a4f8ee986 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -1,38 +1,21 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @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: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\DAV; use Exception; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarObject; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\Directory; -use OCA\DAV\Connector\Sabre\FilesPlugin; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; -use Sabre\CalDAV\ICalendar; use Sabre\DAV\Exception as DavException; use Sabre\DAV\PropertyStorage\Backend\BackendInterface; use Sabre\DAV\PropFind; @@ -83,33 +66,16 @@ class CustomPropertiesBackend implements BackendInterface { '{DAV:}getetag', '{DAV:}quota-used-bytes', '{DAV:}quota-available-bytes', - '{http://owncloud.org/ns}permissions', - '{http://owncloud.org/ns}downloadURL', - '{http://owncloud.org/ns}dDC', - '{http://owncloud.org/ns}size', - '{http://nextcloud.org/ns}is-encrypted', - - // Currently, returning null from any propfind handler would still trigger the backend, - // so we add all known Nextcloud custom properties in here to avoid that - - // text app - '{http://nextcloud.org/ns}rich-workspace', - '{http://nextcloud.org/ns}rich-workspace-file', - // groupfolders - '{http://nextcloud.org/ns}acl-enabled', - '{http://nextcloud.org/ns}acl-can-manage', - '{http://nextcloud.org/ns}acl-list', - '{http://nextcloud.org/ns}inherited-acl-list', - '{http://nextcloud.org/ns}group-folder-id', - // files_lock - '{http://nextcloud.org/ns}lock', - '{http://nextcloud.org/ns}lock-owner-type', - '{http://nextcloud.org/ns}lock-owner', - '{http://nextcloud.org/ns}lock-owner-displayname', - '{http://nextcloud.org/ns}lock-owner-editor', - '{http://nextcloud.org/ns}lock-time', - '{http://nextcloud.org/ns}lock-timeout', - '{http://nextcloud.org/ns}lock-token', + ]; + + /** + * Allowed properties for the oc/nc namespace, all other properties in the namespace are ignored + * + * @var string[] + */ + private const ALLOWED_NC_PROPERTIES = [ + '{http://owncloud.org/ns}calendar-enabled', + '{http://owncloud.org/ns}enabled', ]; /** @@ -131,28 +97,11 @@ class CustomPropertiesBackend implements BackendInterface { ]; /** - * @var Tree - */ - private $tree; - - /** - * @var IDBConnection - */ - private $connection; - - /** - * @var IUser - */ - private $user; - - /** * Properties cache * * @var array */ private $userCache = []; - - private Server $server; private XmlService $xmlService; /** @@ -161,15 +110,12 @@ class CustomPropertiesBackend implements BackendInterface { * @param IUser $user owner of the tree and properties */ public function __construct( - Server $server, - Tree $tree, - IDBConnection $connection, - IUser $user, + private Server $server, + private Tree $tree, + private IDBConnection $connection, + private IUser $user, + private DefaultCalendarValidator $defaultCalendarValidator, ) { - $this->server = $server; - $this->tree = $tree; - $this->connection = $connection; - $this->user = $user; $this->xmlService = new XmlService(); $this->xmlService->elementMap = array_merge( $this->xmlService->elementMap, @@ -187,14 +133,9 @@ class CustomPropertiesBackend implements BackendInterface { public function propFind($path, PropFind $propFind) { $requestedProps = $propFind->get404Properties(); - // these might appear - $requestedProps = array_diff( - $requestedProps, - self::IGNORED_PROPERTIES, - ); $requestedProps = array_filter( $requestedProps, - fn ($prop) => !str_starts_with($prop, FilesPlugin::FILE_METADATA_PREFIX), + $this->isPropertyAllowed(...), ); // substr of calendars/ => path is inside the CalDAV component @@ -256,6 +197,11 @@ class CustomPropertiesBackend implements BackendInterface { $this->cacheDirectory($path, $node); } + if ($node instanceof CalendarObject) { + // No custom properties supported on individual events + return; + } + // First fetch the published properties (set by another user), then get the ones set by // the current user. If both are set then the latter as priority. foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) { @@ -276,6 +222,16 @@ class CustomPropertiesBackend implements BackendInterface { } } + private function isPropertyAllowed(string $property): bool { + if (in_array($property, self::IGNORED_PROPERTIES)) { + return false; + } + if (str_starts_with($property, '{http://owncloud.org/ns}') || str_starts_with($property, '{http://nextcloud.org/ns}')) { + return in_array($property, self::ALLOWED_NC_PROPERTIES); + } + return true; + } + /** * Updates properties for a path * @@ -315,8 +271,8 @@ class CustomPropertiesBackend implements BackendInterface { */ public function move($source, $destination) { $statement = $this->connection->prepare( - 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ?' + 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' + . ' WHERE `userid` = ? AND `propertypath` = ?' ); $statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]); $statement->closeCursor(); @@ -338,10 +294,11 @@ class CustomPropertiesBackend implements BackendInterface { // $path is the principal here as this prop is only set on principals $node = $this->tree->getNodeForPath($href); - if (!($node instanceof ICalendar) || $node->getOwner() !== $path) { + if (!($node instanceof Calendar) || $node->getOwner() !== $path) { throw new DavException('No such calendar'); } + $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node); break; } } @@ -378,16 +335,19 @@ class CustomPropertiesBackend implements BackendInterface { private function cacheDirectory(string $path, Directory $node): void { $prefix = ltrim($path . '/', '/'); $query = $this->connection->getQueryBuilder(); - $query->select('name', 'propertypath', 'propertyname', 'propertyvalue', 'valuetype') + $query->select('name', 'p.propertypath', 'p.propertyname', 'p.propertyvalue', 'p.valuetype') ->from('filecache', 'f') - ->leftJoin('f', 'properties', 'p', $query->expr()->andX( - $query->expr()->eq('propertypath', $query->func()->concat( - $query->createNamedParameter($prefix), - 'name' - )), - $query->expr()->eq('userid', $query->createNamedParameter($this->user->getUID())) - )) - ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT))); + ->hintShardKey('storage', $node->getNode()->getMountPoint()->getNumericStorageId()) + ->leftJoin('f', 'properties', 'p', $query->expr()->eq('p.propertypath', $query->func()->concat( + $query->createNamedParameter($prefix), + 'f.name' + )), + ) + ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->orX( + $query->expr()->eq('p.userid', $query->createNamedParameter($this->user->getUID())), + $query->expr()->isNull('p.userid'), + )); $result = $query->executeQuery(); $propsByPath = []; @@ -430,7 +390,7 @@ class CustomPropertiesBackend implements BackendInterface { // request only a subset $sql .= ' AND `propertyname` in (?)'; $whereValues[] = $requestedProperties; - $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY; + $whereTypes[] = IQueryBuilder::PARAM_STR_ARRAY; } $result = $this->connection->executeQuery( @@ -556,7 +516,9 @@ class CustomPropertiesBackend implements BackendInterface { $value = $value->getHref(); } else { $valueType = self::PROPERTY_TYPE_OBJECT; - $value = serialize($value); + // serialize produces null character + // these can not be properly stored in some databases and need to be replaced + $value = str_replace(chr(0), '\x00', serialize($value)); } return [$value, $valueType]; } @@ -571,7 +533,9 @@ class CustomPropertiesBackend implements BackendInterface { case self::PROPERTY_TYPE_HREF: return new Href($value); case self::PROPERTY_TYPE_OBJECT: - return unserialize($value); + // some databases can not handel null characters, these are custom encoded during serialization + // this custom encoding needs to be first reversed before unserializing + return unserialize(str_replace('\x00', chr(0), $value)); case self::PROPERTY_TYPE_STRING: default: return $value; diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php index 8c126e6b71c..77ba45182c9 100644 --- a/apps/dav/lib/DAV/GroupPrincipalBackend.php +++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2018, Georg Ehrke - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@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: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\DAV; @@ -42,32 +21,17 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface; class GroupPrincipalBackend implements BackendInterface { public const PRINCIPAL_PREFIX = 'principals/groups'; - /** @var IGroupManager */ - private $groupManager; - - /** @var IUserSession */ - private $userSession; - - /** @var IShareManager */ - private $shareManager; - /** @var IConfig */ - private $config; - /** - * @param IGroupManager $IGroupManager + * @param IGroupManager $groupManager * @param IUserSession $userSession * @param IShareManager $shareManager */ public function __construct( - IGroupManager $IGroupManager, - IUserSession $userSession, - IShareManager $shareManager, - IConfig $config + private IGroupManager $groupManager, + private IUserSession $userSession, + private IShareManager $shareManager, + private IConfig $config, ) { - $this->groupManager = $IGroupManager; - $this->userSession = $userSession; - $this->shareManager = $shareManager; - $this->config = $config; } /** @@ -87,8 +51,10 @@ class GroupPrincipalBackend implements BackendInterface { $principals = []; if ($prefixPath === self::PRINCIPAL_PREFIX) { - foreach ($this->groupManager->search('') as $user) { - $principals[] = $this->groupToPrincipal($user); + foreach ($this->groupManager->search('') as $group) { + if (!$group->hideFromCollaboration()) { + $principals[] = $this->groupToPrincipal($group); + } } } @@ -114,7 +80,7 @@ class GroupPrincipalBackend implements BackendInterface { $name = urldecode($elements[2]); $group = $this->groupManager->get($name); - if (!is_null($group)) { + if ($group !== null && !$group->hideFromCollaboration()) { return $this->groupToPrincipal($group); } @@ -223,6 +189,10 @@ class GroupPrincipalBackend implements BackendInterface { $groups = $this->groupManager->search($value, $searchLimit); $results[] = array_reduce($groups, function (array $carry, IGroup $group) use ($restrictGroups) { + if ($group->hideFromCollaboration()) { + return $carry; + } + $gid = $group->getGID(); // is sharing restricted to groups only? if ($restrictGroups !== false) { diff --git a/apps/dav/lib/DAV/PublicAuth.php b/apps/dav/lib/DAV/PublicAuth.php index 3ba8bb2f3c5..c2b4ada173a 100644 --- a/apps/dav/lib/DAV/PublicAuth.php +++ b/apps/dav/lib/DAV/PublicAuth.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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\DAV; @@ -68,9 +53,9 @@ class PublicAuth implements BackendInterface { */ public function check(RequestInterface $request, ResponseInterface $response) { if ($this->isRequestPublic($request)) { - return [true, "principals/system/public"]; + return [true, 'principals/system/public']; } - return [false, "No public access to this resource."]; + return [false, 'No public access to this resource.']; } /** diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index b467479bc1e..d60f5cca7c6 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -2,32 +2,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Anna Larch <anna.larch@gmx.net> - * - * @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\DAV\Sharing; @@ -50,7 +27,8 @@ abstract class Backend { private ICache $shareCache; - public function __construct(private IUserManager $userManager, + public function __construct( + private IUserManager $userManager, private IGroupManager $groupManager, private Principal $principalBackend, private ICacheFactory $cacheFactory, @@ -81,13 +59,13 @@ abstract class Backend { } // Don't add share for owner - if($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) { + if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) { continue; } $principalparts[2] = urldecode($principalparts[2]); - if (($principalparts[1] === 'users' && !$this->userManager->userExists($principalparts[2])) || - ($principalparts[1] === 'groups' && !$this->groupManager->groupExists($principalparts[2]))) { + if (($principalparts[1] === 'users' && !$this->userManager->userExists($principalparts[2])) + || ($principalparts[1] === 'groups' && !$this->groupManager->groupExists($principalparts[2]))) { // User or group does not exist continue; } @@ -106,20 +84,12 @@ abstract class Backend { } // Don't add unshare for owner - if($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) { + if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) { continue; } // Delete any possible direct shares (since the frontend does not separate between them) $this->service->deleteShare($shareable->getResourceId(), $principal); - - // Check if a user has a groupshare that they're trying to free themselves from - // If so we need to add a self::ACCESS_UNSHARED row - if(!str_contains($principal, 'group') - && $this->service->hasGroupShare($oldShares) - ) { - $this->service->unshare($shareable->getResourceId(), $principal); - } } } @@ -153,15 +123,15 @@ abstract class Backend { $rows = $this->service->getShares($resourceId); $shares = []; - foreach($rows as $row) { + foreach ($rows as $row) { $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); $shares[] = [ 'href' => "principal:{$row['principaluri']}", 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', 'status' => 1, - 'readOnly' => (int) $row['access'] === Backend::ACCESS_READ, + 'readOnly' => (int)$row['access'] === Backend::ACCESS_READ, '{http://owncloud.org/ns}principal' => (string)$row['principaluri'], - '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups') + '{http://owncloud.org/ns}group-share' => isset($p['uri']) && (str_starts_with($p['uri'], 'principals/groups') || str_starts_with($p['uri'], 'principals/circles')) ]; } $this->shareCache->set((string)$resourceId, $shares); @@ -178,14 +148,14 @@ abstract class Backend { $rows = $this->service->getSharesForIds($resourceIds); $sharesByResource = array_fill_keys($resourceIds, []); - foreach($rows as $row) { + foreach ($rows as $row) { $resourceId = (int)$row['resourceid']; $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); $sharesByResource[$resourceId][] = [ 'href' => "principal:{$row['principaluri']}", 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', 'status' => 1, - 'readOnly' => (int) $row['access'] === self::ACCESS_READ, + 'readOnly' => (int)$row['access'] === self::ACCESS_READ, '{http://owncloud.org/ns}principal' => (string)$row['principaluri'], '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups') ]; @@ -226,4 +196,45 @@ abstract class Backend { } return $acl; } + + public function unshare(IShareable $shareable, string $principalUri): bool { + $this->shareCache->clear(); + + $principal = $this->principalBackend->findByUri($principalUri, ''); + if (empty($principal)) { + return false; + } + + if ($shareable->getOwner() === $principal) { + return false; + } + + // Delete any possible direct shares (since the frontend does not separate between them) + $this->service->deleteShare($shareable->getResourceId(), $principal); + + $needsUnshare = $this->hasAccessByGroupOrCirclesMembership( + $shareable->getResourceId(), + $principal + ); + + if ($needsUnshare) { + $this->service->unshare($shareable->getResourceId(), $principal); + } + + return true; + } + + private function hasAccessByGroupOrCirclesMembership(int $resourceId, string $principal) { + $memberships = array_merge( + $this->principalBackend->getGroupMembership($principal, true), + $this->principalBackend->getCircleMembership($principal) + ); + + $shares = array_column( + $this->service->getShares($resourceId), + 'principaluri' + ); + + return count(array_intersect($memberships, $shares)) > 0; + } } diff --git a/apps/dav/lib/DAV/Sharing/IShareable.php b/apps/dav/lib/DAV/Sharing/IShareable.php index 759981af078..d83079f6975 100644 --- a/apps/dav/lib/DAV/Sharing/IShareable.php +++ b/apps/dav/lib/DAV/Sharing/IShareable.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\DAV\Sharing; diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php index 78e086bc907..03e63813bab 100644 --- a/apps/dav/lib/DAV/Sharing/Plugin.php +++ b/apps/dav/lib/DAV/Sharing/Plugin.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @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\DAV\Sharing; @@ -29,6 +12,7 @@ use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\DAV\Sharing\Xml\Invite; use OCA\DAV\DAV\Sharing\Xml\ShareRequest; +use OCP\AppFramework\Http; use OCP\IConfig; use OCP\IRequest; use Sabre\DAV\Exception\NotFound; @@ -43,26 +27,18 @@ class Plugin extends ServerPlugin { public const NS_OWNCLOUD = 'http://owncloud.org/ns'; public const NS_NEXTCLOUD = 'http://nextcloud.com/ns'; - /** @var Auth */ - private $auth; - - /** @var IRequest */ - private $request; - - /** @var IConfig */ - private $config; - /** * Plugin constructor. * - * @param Auth $authBackEnd + * @param Auth $auth * @param IRequest $request * @param IConfig $config */ - public function __construct(Auth $authBackEnd, IRequest $request, IConfig $config) { - $this->auth = $authBackEnd; - $this->request = $request; - $this->config = $config; + public function __construct( + private Auth $auth, + private IRequest $request, + private IConfig $config, + ) { } /** @@ -127,7 +103,7 @@ class Plugin extends ServerPlugin { $path = $request->getPath(); // Only handling xml - $contentType = (string) $request->getHeader('Content-Type'); + $contentType = (string)$request->getHeader('Content-Type'); if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) { return; } @@ -182,7 +158,7 @@ class Plugin extends ServerPlugin { $node->updateShares($message->set, $message->remove); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); // Adding this because sending a response body may cause issues, // and I wanted some type of indicator the response was handled. $response->setHeader('X-Sabre-Status', 'everything-went-well'); diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php index c0c939c7a5e..e4722208189 100644 --- a/apps/dav/lib/DAV/Sharing/SharingMapper.php +++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php @@ -1,49 +1,49 @@ <?php declare(strict_types=1); -/* - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\DAV\DAV\Sharing; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; class SharingMapper { - public function __construct(private IDBConnection $db) { + public function __construct( + private IDBConnection $db, + ) { } - public function getSharesForId(int $resourceId, string $resourceType): array { + protected function getSharesForIdByAccess(int $resourceId, string $resourceType, bool $sharesWithAccess): array { $query = $this->db->getQueryBuilder(); - $result = $query->select(['principaluri', 'access']) + $query->select(['principaluri', 'access']) ->from('dav_shares') ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR))) - ->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT))) - ->groupBy(['principaluri', 'access']) - ->executeQuery(); + ->groupBy(['principaluri', 'access']); + if ($sharesWithAccess) { + $query->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT))); + } else { + $query->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT))); + } + + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); return $rows; } + public function getSharesForId(int $resourceId, string $resourceType): array { + return $this->getSharesForIdByAccess($resourceId, $resourceType, true); + } + + public function getUnsharesForId(int $resourceId, string $resourceType): array { + return $this->getSharesForIdByAccess($resourceId, $resourceType, false); + } + public function getSharesForIds(array $resourceIds, string $resourceType): array { $query = $this->db->getQueryBuilder(); $result = $query->select(['resourceid', 'principaluri', 'access']) @@ -110,4 +110,28 @@ class SharingMapper { ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType))) ->executeStatement(); } + + public function getSharesByPrincipals(array $principals, string $resourceType): array { + $query = $this->db->getQueryBuilder(); + $result = $query->select(['id', 'principaluri', 'type', 'access', 'resourceid']) + ->from('dav_shares') + ->where($query->expr()->in('principaluri', $query->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY)) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType))) + ->orderBy('id') + ->executeQuery(); + + $rows = $result->fetchAll(); + $result->closeCursor(); + + return $rows; + } + + public function deleteUnsharesByPrincipal(string $principal, string $resourceType): void { + $query = $this->db->getQueryBuilder(); + $query->delete('dav_shares') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) + ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType))) + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } } diff --git a/apps/dav/lib/DAV/Sharing/SharingService.php b/apps/dav/lib/DAV/Sharing/SharingService.php index 4b2a0beed1c..11459e12d74 100644 --- a/apps/dav/lib/DAV/Sharing/SharingService.php +++ b/apps/dav/lib/DAV/Sharing/SharingService.php @@ -2,28 +2,16 @@ declare(strict_types=1); /** - * @copyright 2024 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\DAV\Sharing; abstract class SharingService { protected string $resourceType = ''; - public function __construct(protected SharingMapper $mapper) { + public function __construct( + protected SharingMapper $mapper, + ) { } public function getResourceType(): string { @@ -55,17 +43,11 @@ abstract class SharingService { return $this->mapper->getSharesForId($resourceId, $this->getResourceType()); } - public function getSharesForIds(array $resourceIds): array { - return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType()); + public function getUnshares(int $resourceId): array { + return $this->mapper->getUnsharesForId($resourceId, $this->getResourceType()); } - /** - * @param array $oldShares - * @return bool - */ - public function hasGroupShare(array $oldShares): bool { - return !empty(array_filter($oldShares, function (array $share) { - return $share['{http://owncloud.org/ns}group-share'] === true; - })); + public function getSharesForIds(array $resourceIds): array { + return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType()); } } diff --git a/apps/dav/lib/DAV/Sharing/Xml/Invite.php b/apps/dav/lib/DAV/Sharing/Xml/Invite.php index 3c03dfa5d25..7a20dbe6df7 100644 --- a/apps/dav/lib/DAV/Sharing/Xml/Invite.php +++ b/apps/dav/lib/DAV/Sharing/Xml/Invite.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (C) fruux GmbH (https://fruux.com/) - * @copyright Copyright (C) fruux GmbH (https://fruux.com/) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @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-FileCopyrightText: fruux GmbH (https://fruux.com/) + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\DAV\Sharing\Xml; @@ -45,21 +27,6 @@ use Sabre\Xml\XmlSerializable; class Invite implements XmlSerializable { /** - * The list of users a calendar has been shared to. - * - * @var array - */ - protected $users; - - /** - * The organizer contains information about the person who shared the - * object. - * - * @var array|null - */ - protected $organizer; - - /** * Creates the property. * * Users is an array. Each element of the array has the following @@ -85,9 +52,17 @@ class Invite implements XmlSerializable { * * @param array $users */ - public function __construct(array $users, ?array $organizer = null) { - $this->users = $users; - $this->organizer = $organizer; + public function __construct( + /** + * The list of users a calendar has been shared to. + */ + protected array $users, + /** + * The organizer contains information about the person who shared the + * object. + */ + protected ?array $organizer = null, + ) { } /** diff --git a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php index 6a97cd30d9f..aefb39c5701 100644 --- a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php +++ b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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\DAV\Sharing\Xml; @@ -27,24 +12,21 @@ use Sabre\Xml\Reader; use Sabre\Xml\XmlDeserializable; class ShareRequest implements XmlDeserializable { - public $set = []; - - public $remove = []; - /** * Constructor * * @param array $set * @param array $remove */ - public function __construct(array $set, array $remove) { - $this->set = $set; - $this->remove = $remove; + public function __construct( + public array $set, + public array $remove, + ) { } public static function xmlDeserialize(Reader $reader) { $elements = $reader->parseInnerTree([ - '{' . Plugin::NS_OWNCLOUD. '}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{' . Plugin::NS_OWNCLOUD . '}set' => 'Sabre\\Xml\\Element\\KeyValue', '{' . Plugin::NS_OWNCLOUD . '}remove' => 'Sabre\\Xml\\Element\\KeyValue', ]); diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php index d5739212e86..9760d68f05f 100644 --- a/apps/dav/lib/DAV/SystemPrincipalBackend.php +++ b/apps/dav/lib/DAV/SystemPrincipalBackend.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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\DAV; diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php index 389dd96efb4..9b9615b8063 100644 --- a/apps/dav/lib/DAV/ViewOnlyPlugin.php +++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php @@ -1,22 +1,9 @@ <?php + /** - * @author Piotr Mrowczynski piotr@owncloud.com - * - * @copyright Copyright (c) 2019, ownCloud GmbH - * @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: 2022-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2019 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\DAV; @@ -26,6 +13,7 @@ use OCA\DAV\Connector\Sabre\File as DavFile; use OCA\Files_Versions\Sabre\VersionFile; use OCP\Files\Folder; use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -36,12 +24,10 @@ use Sabre\HTTP\RequestInterface; */ class ViewOnlyPlugin extends ServerPlugin { private ?Server $server = null; - private ?Folder $userFolder; public function __construct( - ?Folder $userFolder, + private ?Folder $userFolder, ) { - $this->userFolder = $userFolder; } /** @@ -58,6 +44,7 @@ class ViewOnlyPlugin extends ServerPlugin { //Sabre\DAV\CorePlugin::httpGet $this->server->on('method:GET', [$this, 'checkViewOnly'], 90); $this->server->on('method:COPY', [$this, 'checkViewOnly'], 90); + $this->server->on('method:MOVE', [$this, 'checkViewOnly'], 90); } /** @@ -85,7 +72,7 @@ class ViewOnlyPlugin extends ServerPlugin { $nodes = $this->userFolder->getById($node->getId()); $node = array_pop($nodes); if (!$node) { - throw new NotFoundException("Version file not accessible by current user"); + throw new NotFoundException('Version file not accessible by current user'); } } } else { @@ -94,21 +81,28 @@ class ViewOnlyPlugin extends ServerPlugin { $storage = $node->getStorage(); - if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) { + if (!$storage->instanceOfStorage(ISharedStorage::class)) { return true; } + // Extract extra permissions - /** @var \OCA\Files_Sharing\SharedStorage $storage */ + /** @var ISharedStorage $storage */ $share = $storage->getShare(); - $attributes = $share->getAttributes(); if ($attributes === null) { return true; } - // Check if read-only and on whether permission can download is both set and disabled. + // We have two options here, if download is disabled, but viewing is allowed, + // we still allow the GET request to return the file content. $canDownload = $attributes->getAttribute('permissions', 'download'); - if ($canDownload !== null && !$canDownload) { + if (!$share->canSeeContent()) { + throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.'); + } + + // If download is disabled, we disable the COPY and MOVE methods even if the + // shareapi_allow_view_without_download is set to true. + if ($request->getMethod() !== 'GET' && ($canDownload !== null && !$canDownload)) { throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.'); } } catch (NotFound $e) { diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index dc9afb59653..d7cd46087c3 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; @@ -46,6 +29,10 @@ use OCP\User\IOutOfOfficeData; * @method void setStatus(string $status) * @method string getMessage() * @method void setMessage(string $message) + * @method string getReplacementUserId() + * @method void setReplacementUserId(?string $replacementUserId) + * @method string getReplacementUserDisplayName() + * @method void setReplacementUserDisplayName(?string $replacementUserDisplayName) */ class Absence extends Entity implements JsonSerializable { protected string $userId = ''; @@ -60,17 +47,23 @@ class Absence extends Entity implements JsonSerializable { protected string $message = ''; + protected ?string $replacementUserId = null; + + protected ?string $replacementUserDisplayName = null; + public function __construct() { $this->addType('userId', 'string'); $this->addType('firstDay', 'string'); $this->addType('lastDay', 'string'); $this->addType('status', 'string'); $this->addType('message', 'string'); + $this->addType('replacementUserId', 'string'); + $this->addType('replacementUserDisplayName', 'string'); } public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeData { if ($user->getUID() !== $this->getUserId()) { - throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID()); + throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ', got ' . $user->getUID()); } if ($this->getId() === null) { throw new Exception('Creating out-of-office data without ID'); @@ -87,6 +80,8 @@ class Absence extends Entity implements JsonSerializable { $endDate->getTimestamp(), $this->getStatus(), $this->getMessage(), + $this->getReplacementUserId(), + $this->getReplacementUserDisplayName(), ); } @@ -97,6 +92,8 @@ class Absence extends Entity implements JsonSerializable { 'lastDay' => $this->lastDay, 'status' => $this->status, 'message' => $this->message, + 'replacementUserId' => $this->replacementUserId, + 'replacementUserDisplayName' => $this->replacementUserDisplayName, ]; } } diff --git a/apps/dav/lib/Db/AbsenceMapper.php b/apps/dav/lib/Db/AbsenceMapper.php index 7529d04cf10..1214a123236 100644 --- a/apps/dav/lib/Db/AbsenceMapper.php +++ b/apps/dav/lib/Db/AbsenceMapper.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; diff --git a/apps/dav/lib/Db/Direct.php b/apps/dav/lib/Db/Direct.php index ca2586ab2e0..4e4a12d225f 100644 --- a/apps/dav/lib/Db/Direct.php +++ b/apps/dav/lib/Db/Direct.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; /** * @method string getUserId() @@ -51,9 +35,9 @@ class Direct extends Entity { protected $expiration; public function __construct() { - $this->addType('userId', 'string'); - $this->addType('fileId', 'int'); - $this->addType('token', 'string'); - $this->addType('expiration', 'int'); + $this->addType('userId', Types::STRING); + $this->addType('fileId', Types::INTEGER); + $this->addType('token', Types::STRING); + $this->addType('expiration', Types::INTEGER); } } diff --git a/apps/dav/lib/Db/DirectMapper.php b/apps/dav/lib/Db/DirectMapper.php index c0ed10b97c6..4fedac35b72 100644 --- a/apps/dav/lib/Db/DirectMapper.php +++ b/apps/dav/lib/Db/DirectMapper.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; @@ -62,6 +45,6 @@ class DirectMapper extends QBMapper { $qb->expr()->lt('expiration', $qb->createNamedParameter($expiration)) ); - $qb->execute(); + $qb->executeStatement(); } } diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php index 5234ad852b1..96c5f75ef4f 100644 --- a/apps/dav/lib/Db/Property.php +++ b/apps/dav/lib/Db/Property.php @@ -2,25 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php index 6f39d03d1c1..1789194ee7a 100644 --- a/apps/dav/lib/Db/PropertyMapper.php +++ b/apps/dav/lib/Db/PropertyMapper.php @@ -2,25 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Db; @@ -54,4 +38,18 @@ class PropertyMapper extends QBMapper { return $this->findEntities($selectQb); } + /** + * @return Property[] + */ + public function findPropertiesByPath(string $userId, string $path): array { + $selectQb = $this->db->getQueryBuilder(); + $selectQb->select('*') + ->from(self::TABLE_NAME) + ->where( + $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)), + $selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)), + ); + return $this->findEntities($selectQb); + } + } diff --git a/apps/dav/lib/Direct/DirectFile.php b/apps/dav/lib/Direct/DirectFile.php index b9d1757cfc2..7f41dd65f41 100644 --- a/apps/dav/lib/Direct/DirectFile.php +++ b/apps/dav/lib/Direct/DirectFile.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Direct; @@ -36,21 +18,14 @@ use Sabre\DAV\Exception\NotFound; use Sabre\DAV\IFile; class DirectFile implements IFile { - /** @var Direct */ - private $direct; - - /** @var IRootFolder */ - private $rootFolder; - /** @var File */ private $file; - private $eventDispatcher; - - public function __construct(Direct $direct, IRootFolder $rootFolder, IEventDispatcher $eventDispatcher) { - $this->direct = $direct; - $this->rootFolder = $rootFolder; - $this->eventDispatcher = $eventDispatcher; + public function __construct( + private Direct $direct, + private IRootFolder $rootFolder, + private IEventDispatcher $eventDispatcher, + ) { } public function put($data) { @@ -114,7 +89,7 @@ class DirectFile implements IFile { throw new NotFound(); } if (!$file instanceof File) { - throw new Forbidden("direct download not allowed on directories"); + throw new Forbidden('direct download not allowed on directories'); } $this->file = $file; diff --git a/apps/dav/lib/Direct/DirectHome.php b/apps/dav/lib/Direct/DirectHome.php index 8fc8b555db5..ac411c9b52f 100644 --- a/apps/dav/lib/Direct/DirectHome.php +++ b/apps/dav/lib/Direct/DirectHome.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Direct; @@ -40,38 +22,14 @@ use Sabre\DAV\ICollection; class DirectHome implements ICollection { - /** @var IRootFolder */ - private $rootFolder; - - /** @var DirectMapper */ - private $mapper; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var IThrottler */ - private $throttler; - - /** @var IRequest */ - private $request; - - /** @var IEventDispatcher */ - private $eventDispatcher; - public function __construct( - IRootFolder $rootFolder, - DirectMapper $mapper, - ITimeFactory $timeFactory, - IThrottler $throttler, - IRequest $request, - IEventDispatcher $eventDispatcher + private IRootFolder $rootFolder, + private DirectMapper $mapper, + private ITimeFactory $timeFactory, + private IThrottler $throttler, + private IRequest $request, + private IEventDispatcher $eventDispatcher, ) { - $this->rootFolder = $rootFolder; - $this->mapper = $mapper; - $this->timeFactory = $timeFactory; - $this->throttler = $throttler; - $this->request = $request; - $this->eventDispatcher = $eventDispatcher; } public function createFile($name, $data = null) { @@ -95,7 +53,7 @@ class DirectHome implements ICollection { } catch (DoesNotExistException $e) { // Since the token space is so huge only throttle on non-existing token $this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress()); - $this->throttler->sleepDelay($this->request->getRemoteAddress(), 'directlink'); + $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), 'directlink'); throw new NotFound(); } diff --git a/apps/dav/lib/Direct/Server.php b/apps/dav/lib/Direct/Server.php index 0ce5798571d..957f6f99b34 100644 --- a/apps/dav/lib/Direct/Server.php +++ b/apps/dav/lib/Direct/Server.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Direct; diff --git a/apps/dav/lib/Direct/ServerFactory.php b/apps/dav/lib/Direct/ServerFactory.php index f9914a5cb36..473439361c2 100644 --- a/apps/dav/lib/Direct/ServerFactory.php +++ b/apps/dav/lib/Direct/ServerFactory.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Direct; @@ -39,17 +20,15 @@ use OCP\L10N\IFactory; use OCP\Security\Bruteforce\IThrottler; class ServerFactory { - /** @var IConfig */ - private $config; /** @var IL10N */ private $l10n; - /** @var IEventDispatcher */ - private $eventDispatcher; - public function __construct(IConfig $config, IFactory $l10nFactory, IEventDispatcher $eventDispatcher) { - $this->config = $config; + public function __construct( + private IConfig $config, + IFactory $l10nFactory, + private IEventDispatcher $eventDispatcher, + ) { $this->l10n = $l10nFactory->get('dav'); - $this->eventDispatcher = $eventDispatcher; } public function createServer(string $baseURI, diff --git a/apps/dav/lib/Events/AddressBookCreatedEvent.php b/apps/dav/lib/Events/AddressBookCreatedEvent.php index 396c246289c..1a56bcbf63f 100644 --- a/apps/dav/lib/Events/AddressBookCreatedEvent.php +++ b/apps/dav/lib/Events/AddressBookCreatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event; */ class AddressBookCreatedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - /** * AddressBookCreatedEvent constructor. * @@ -48,11 +25,11 @@ class AddressBookCreatedEvent extends Event { * @param array $addressBookData * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; } /** diff --git a/apps/dav/lib/Events/AddressBookDeletedEvent.php b/apps/dav/lib/Events/AddressBookDeletedEvent.php index 79785f5f0fa..b1ec4125513 100644 --- a/apps/dav/lib/Events/AddressBookDeletedEvent.php +++ b/apps/dav/lib/Events/AddressBookDeletedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event; */ class AddressBookDeletedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $shares; - /** * AddressBookDeletedEvent constructor. * @@ -52,13 +26,12 @@ class AddressBookDeletedEvent extends Event { * @param array $shares * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $shares) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $shares, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->shares = $shares; } /** diff --git a/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php b/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php index da3c2701abd..9a574fb548e 100644 --- a/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php +++ b/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,21 +18,6 @@ use OCP\EventDispatcher\Event; */ class AddressBookShareUpdatedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $oldShares; - - /** @var array */ - private $added; - - /** @var array */ - private $removed; - /** * AddressBookShareUpdatedEvent constructor. * @@ -60,17 +28,14 @@ class AddressBookShareUpdatedEvent extends Event { * @param array $removed * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $oldShares, - array $added, - array $removed) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $oldShares, + private array $added, + private array $removed, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->oldShares = $oldShares; - $this->added = $added; - $this->removed = $removed; } /** diff --git a/apps/dav/lib/Events/AddressBookUpdatedEvent.php b/apps/dav/lib/Events/AddressBookUpdatedEvent.php index d651e569467..fe6dc024cd2 100644 --- a/apps/dav/lib/Events/AddressBookUpdatedEvent.php +++ b/apps/dav/lib/Events/AddressBookUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class AddressBookUpdatedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $shares; - - /** @var array */ - private $mutations; - /** * AddressBookUpdatedEvent constructor. * @@ -56,15 +27,13 @@ class AddressBookUpdatedEvent extends Event { * @param array $mutations * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $shares, - array $mutations) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $shares, + private array $mutations, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->shares = $shares; - $this->mutations = $mutations; } /** diff --git a/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php b/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php index ddb79505ac6..a79d730e8ff 100644 --- a/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php +++ b/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -32,11 +15,10 @@ use OCP\Files\File; * @since 22.0.0 */ class BeforeFileDirectDownloadedEvent extends Event { - private $file; - - public function __construct(File $file) { + public function __construct( + private File $file, + ) { parent::__construct(); - $this->file = $file; } /** diff --git a/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php index 64d9b6dd2c7..ea1c344ed27 100644 --- a/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php +++ b/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CachedCalendarObjectCreatedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - /** * CachedCalendarObjectCreatedEvent constructor. * @@ -56,15 +27,13 @@ class CachedCalendarObjectCreatedEvent extends Event { * @param array $objectData * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData, - array $shares, - array $objectData) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + private array $shares, + private array $objectData, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; - $this->shares = $shares; - $this->objectData = $objectData; } /** diff --git a/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php index 183e8e8bcf9..8f8e55d32e5 100644 --- a/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php +++ b/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CachedCalendarObjectDeletedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - /** * CachedCalendarObjectDeletedEvent constructor. * @@ -56,15 +27,13 @@ class CachedCalendarObjectDeletedEvent extends Event { * @param array $objectData * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData, - array $shares, - array $objectData) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + private array $shares, + private array $objectData, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; - $this->shares = $shares; - $this->objectData = $objectData; } /** diff --git a/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php index 62781483def..0adb4164dc9 100644 --- a/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php +++ b/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CachedCalendarObjectUpdatedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - /** * CachedCalendarObjectUpdatedEvent constructor. * @@ -56,15 +27,13 @@ class CachedCalendarObjectUpdatedEvent extends Event { * @param array $objectData * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData, - array $shares, - array $objectData) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + private array $shares, + private array $objectData, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; - $this->shares = $shares; - $this->objectData = $objectData; } /** diff --git a/apps/dav/lib/Events/CalendarCreatedEvent.php b/apps/dav/lib/Events/CalendarCreatedEvent.php index 649a242a1d2..46d1194914e 100644 --- a/apps/dav/lib/Events/CalendarCreatedEvent.php +++ b/apps/dav/lib/Events/CalendarCreatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event; */ class CalendarCreatedEvent extends Event { - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - /** * CalendarCreatedEvent constructor. * @@ -48,11 +25,11 @@ class CalendarCreatedEvent extends Event { * @param array $calendarData * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData) { + public function __construct( + private int $calendarId, + private array $calendarData, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; } /** diff --git a/apps/dav/lib/Events/CalendarDeletedEvent.php b/apps/dav/lib/Events/CalendarDeletedEvent.php index 97d522cde7c..c8ab4265b27 100644 --- a/apps/dav/lib/Events/CalendarDeletedEvent.php +++ b/apps/dav/lib/Events/CalendarDeletedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event; */ class CalendarDeletedEvent extends Event { - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - /** * CalendarDeletedEvent constructor. * @@ -52,13 +26,12 @@ class CalendarDeletedEvent extends Event { * @param array $shares * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - array $shares) { + public function __construct( + private int $calendarId, + private array $calendarData, + private array $shares, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; } /** diff --git a/apps/dav/lib/Events/CalendarMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php index 5fb2f48a75c..8bb660a98c6 100644 --- a/apps/dav/lib/Events/CalendarMovedToTrashEvent.php +++ b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -32,28 +15,18 @@ use OCP\EventDispatcher\Event; */ class CalendarMovedToTrashEvent extends Event { - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - /** * @param int $calendarId * @param array $calendarData * @param array $shares * @since 22.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - array $shares) { + public function __construct( + private int $calendarId, + private array $calendarData, + private array $shares, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; } /** diff --git a/apps/dav/lib/Events/CalendarObjectCreatedEvent.php b/apps/dav/lib/Events/CalendarObjectCreatedEvent.php deleted file mode 100644 index 123f7fc229f..00000000000 --- a/apps/dav/lib/Events/CalendarObjectCreatedEvent.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * Class CalendarObjectCreatedEvent - * - * @package OCA\DAV\Events - * @since 20.0.0 - */ -class CalendarObjectCreatedEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - - /** - * CalendarObjectCreatedEvent constructor. - * - * @param int $calendarId - * @param array $calendarData - * @param array $shares - * @param array $objectData - * @since 20.0.0 - */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $objectData) { - parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 20.0.0 - */ - public function getCalendarId(): int { - return $this->calendarId; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getCalendarData(): array { - return $this->calendarData; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getShares(): array { - return $this->shares; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarObjectDeletedEvent.php b/apps/dav/lib/Events/CalendarObjectDeletedEvent.php deleted file mode 100644 index 8c5834ff050..00000000000 --- a/apps/dav/lib/Events/CalendarObjectDeletedEvent.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * Class CalendarObjectDeletedEvent - * - * @package OCA\DAV\Events - * @since 20.0.0 - */ -class CalendarObjectDeletedEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - - /** - * CalendarObjectDeletedEvent constructor. - * - * @param int $calendarId - * @param array $calendarData - * @param array $shares - * @param array $objectData - * @since 20.0.0 - */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $objectData) { - parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 20.0.0 - */ - public function getCalendarId(): int { - return $this->calendarId; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getCalendarData(): array { - return $this->calendarData; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getShares(): array { - return $this->shares; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarObjectMovedEvent.php b/apps/dav/lib/Events/CalendarObjectMovedEvent.php deleted file mode 100644 index 402a9adf979..00000000000 --- a/apps/dav/lib/Events/CalendarObjectMovedEvent.php +++ /dev/null @@ -1,120 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * Class CalendarObjectMovedEvent - * - * @package OCA\DAV\Events - * @since 25.0.0 - */ -class CalendarObjectMovedEvent extends Event { - private int $sourceCalendarId; - private array $sourceCalendarData; - private int $targetCalendarId; - private array $targetCalendarData; - private array $sourceShares; - private array $targetShares; - private array $objectData; - - /** - * @since 25.0.0 - */ - public function __construct(int $sourceCalendarId, - array $sourceCalendarData, - int $targetCalendarId, - array $targetCalendarData, - array $sourceShares, - array $targetShares, - array $objectData) { - parent::__construct(); - $this->sourceCalendarId = $sourceCalendarId; - $this->sourceCalendarData = $sourceCalendarData; - $this->targetCalendarId = $targetCalendarId; - $this->targetCalendarData = $targetCalendarData; - $this->sourceShares = $sourceShares; - $this->targetShares = $targetShares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 25.0.0 - */ - public function getSourceCalendarId(): int { - return $this->sourceCalendarId; - } - - /** - * @return array - * @since 25.0.0 - */ - public function getSourceCalendarData(): array { - return $this->sourceCalendarData; - } - - /** - * @return int - * @since 25.0.0 - */ - public function getTargetCalendarId(): int { - return $this->targetCalendarId; - } - - /** - * @return array - * @since 25.0.0 - */ - public function getTargetCalendarData(): array { - return $this->targetCalendarData; - } - - /** - * @return array - * @since 25.0.0 - */ - public function getSourceShares(): array { - return $this->sourceShares; - } - - /** - * @return array - * @since 25.0.0 - */ - public function getTargetShares(): array { - return $this->targetShares; - } - - /** - * @return array - * @since 25.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php deleted file mode 100644 index b963d1321a4..00000000000 --- a/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * @since 22.0.0 - */ -class CalendarObjectMovedToTrashEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - - /** - * @param int $calendarId - * @param array $calendarData - * @param array $shares - * @param array $objectData - * @since 22.0.0 - */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $objectData) { - parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 22.0.0 - */ - public function getCalendarId(): int { - return $this->calendarId; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getCalendarData(): array { - return $this->calendarData; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getShares(): array { - return $this->shares; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarObjectRestoredEvent.php b/apps/dav/lib/Events/CalendarObjectRestoredEvent.php deleted file mode 100644 index 76589725986..00000000000 --- a/apps/dav/lib/Events/CalendarObjectRestoredEvent.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * @since 22.0.0 - */ -class CalendarObjectRestoredEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - - /** - * @param int $calendarId - * @param array $calendarData - * @param array $shares - * @param array $objectData - * @since 22.0.0 - */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $objectData) { - parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 22.0.0 - */ - public function getCalendarId(): int { - return $this->calendarId; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getCalendarData(): array { - return $this->calendarData; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getShares(): array { - return $this->shares; - } - - /** - * @return array - * @since 22.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php b/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php deleted file mode 100644 index 45d66370137..00000000000 --- a/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\DAV\Events; - -use OCP\EventDispatcher\Event; - -/** - * Class CalendarObjectUpdatedEvent - * - * @package OCA\DAV\Events - * @since 20.0.0 - */ -class CalendarObjectUpdatedEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $objectData; - - /** - * CalendarObjectUpdatedEvent constructor. - * - * @param int $calendarId - * @param array $calendarData - * @param array $shares - * @param array $objectData - * @since 20.0.0 - */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $objectData) { - parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->objectData = $objectData; - } - - /** - * @return int - * @since 20.0.0 - */ - public function getCalendarId(): int { - return $this->calendarId; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getCalendarData(): array { - return $this->calendarData; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getShares(): array { - return $this->shares; - } - - /** - * @return array - * @since 20.0.0 - */ - public function getObjectData(): array { - return $this->objectData; - } -} diff --git a/apps/dav/lib/Events/CalendarPublishedEvent.php b/apps/dav/lib/Events/CalendarPublishedEvent.php index b92d17901f7..32fb1c36963 100644 --- a/apps/dav/lib/Events/CalendarPublishedEvent.php +++ b/apps/dav/lib/Events/CalendarPublishedEvent.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,10 +17,6 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 */ class CalendarPublishedEvent extends Event { - private int $calendarId; - private array $calendarData; - private string $publicUri; - /** * CalendarPublishedEvent constructor. * @@ -47,13 +25,12 @@ class CalendarPublishedEvent extends Event { * @param string $publicUri * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - string $publicUri) { + public function __construct( + private int $calendarId, + private array $calendarData, + private string $publicUri, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->publicUri = $publicUri; } /** diff --git a/apps/dav/lib/Events/CalendarRestoredEvent.php b/apps/dav/lib/Events/CalendarRestoredEvent.php index b44b3607969..f404771dea2 100644 --- a/apps/dav/lib/Events/CalendarRestoredEvent.php +++ b/apps/dav/lib/Events/CalendarRestoredEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -32,28 +15,18 @@ use OCP\EventDispatcher\Event; */ class CalendarRestoredEvent extends Event { - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - /** * @param int $calendarId * @param array $calendarData * @param array $shares * @since 22.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - array $shares) { + public function __construct( + private int $calendarId, + private array $calendarData, + private array $shares, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; } /** diff --git a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php index ebde62a8be2..0f8b23ad3ac 100644 --- a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php +++ b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php @@ -3,74 +3,42 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; +use OCA\DAV\CalDAV\CalDavBackend; use OCP\EventDispatcher\Event; -use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; -use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; /** * Class CalendarShareUpdatedEvent * * @package OCA\DAV\Events * @since 20.0.0 + * + * @psalm-import-type CalendarInfo from CalDavBackend */ class CalendarShareUpdatedEvent extends Event { - private int $calendarId; - - /** @var array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } */ - private array $calendarData; - - /** @var list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - private array $oldShares; - - /** @var list<array{href: string, commonName: string, readOnly: bool}> */ - private array $added; - - /** @var list<string> */ - private array $removed; - /** * CalendarShareUpdatedEvent constructor. * * @param int $calendarId - * @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } $calendarData + * @psalm-param CalendarInfo $calendarData + * @param array $calendarData * @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $oldShares * @param list<array{href: string, commonName: string, readOnly: bool}> $added * @param list<string> $removed * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - array $oldShares, - array $added, - array $removed) { + public function __construct( + private int $calendarId, + private array $calendarData, + private array $oldShares, + private array $added, + private array $removed, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->oldShares = $oldShares; - $this->added = $added; - $this->removed = $removed; } /** @@ -81,7 +49,8 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } + * @psalm-return CalendarInfo + * @return array * @since 20.0.0 */ public function getCalendarData(): array { diff --git a/apps/dav/lib/Events/CalendarUnpublishedEvent.php b/apps/dav/lib/Events/CalendarUnpublishedEvent.php index ede60aeb904..10d1712686d 100644 --- a/apps/dav/lib/Events/CalendarUnpublishedEvent.php +++ b/apps/dav/lib/Events/CalendarUnpublishedEvent.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,9 +17,6 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 */ class CalendarUnpublishedEvent extends Event { - private int $calendarId; - private array $calendarData; - /** * CalendarUnpublishedEvent constructor. * @@ -45,11 +24,11 @@ class CalendarUnpublishedEvent extends Event { * @param array $calendarData * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData) { + public function __construct( + private int $calendarId, + private array $calendarData, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; } /** diff --git a/apps/dav/lib/Events/CalendarUpdatedEvent.php b/apps/dav/lib/Events/CalendarUpdatedEvent.php index c39d22b281c..a603d3152f0 100644 --- a/apps/dav/lib/Events/CalendarUpdatedEvent.php +++ b/apps/dav/lib/Events/CalendarUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CalendarUpdatedEvent extends Event { - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var array */ - private $shares; - - /** @var array */ - private $mutations; - /** * CalendarUpdatedEvent constructor. * @@ -56,15 +27,13 @@ class CalendarUpdatedEvent extends Event { * @param array $mutations * @since 20.0.0 */ - public function __construct(int $calendarId, - array $calendarData, - array $shares, - array $mutations) { + public function __construct( + private int $calendarId, + private array $calendarData, + private array $shares, + private array $mutations, + ) { parent::__construct(); - $this->calendarId = $calendarId; - $this->calendarData = $calendarData; - $this->shares = $shares; - $this->mutations = $mutations; } /** diff --git a/apps/dav/lib/Events/CardCreatedEvent.php b/apps/dav/lib/Events/CardCreatedEvent.php index 138cccd6862..5a66d73e707 100644 --- a/apps/dav/lib/Events/CardCreatedEvent.php +++ b/apps/dav/lib/Events/CardCreatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CardCreatedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $shares; - - /** @var array */ - private $cardData; - /** * CardCreatedEvent constructor. * @@ -56,15 +27,13 @@ class CardCreatedEvent extends Event { * @param array $cardData * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $shares, - array $cardData) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $shares, + private array $cardData, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->shares = $shares; - $this->cardData = $cardData; } /** diff --git a/apps/dav/lib/Events/CardDeletedEvent.php b/apps/dav/lib/Events/CardDeletedEvent.php index e0a92a2076a..237ffa7d623 100644 --- a/apps/dav/lib/Events/CardDeletedEvent.php +++ b/apps/dav/lib/Events/CardDeletedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CardDeletedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $shares; - - /** @var array */ - private $cardData; - /** * CardDeletedEvent constructor. * @@ -56,15 +27,13 @@ class CardDeletedEvent extends Event { * @param array $cardData * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $shares, - array $cardData) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $shares, + private array $cardData, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->shares = $shares; - $this->cardData = $cardData; } /** diff --git a/apps/dav/lib/Events/CardMovedEvent.php b/apps/dav/lib/Events/CardMovedEvent.php index 38a66ed08b6..be69a046537 100644 --- a/apps/dav/lib/Events/CardMovedEvent.php +++ b/apps/dav/lib/Events/CardMovedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023, Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -34,32 +17,19 @@ use OCP\EventDispatcher\Event; * @since 27.0.0 */ class CardMovedEvent extends Event { - private int $sourceAddressBookId; - private array $sourceAddressBookData; - private int $targetAddressBookId; - private array $targetAddressBookData; - private array $sourceShares; - private array $targetShares; - private array $objectData; - /** * @since 27.0.0 */ - public function __construct(int $sourceAddressBookId, - array $sourceAddressBookData, - int $targetAddressBookId, - array $targetAddressBookData, - array $sourceShares, - array $targetShares, - array $objectData) { + public function __construct( + private int $sourceAddressBookId, + private array $sourceAddressBookData, + private int $targetAddressBookId, + private array $targetAddressBookData, + private array $sourceShares, + private array $targetShares, + private array $objectData, + ) { parent::__construct(); - $this->sourceAddressBookId = $sourceAddressBookId; - $this->sourceAddressBookData = $sourceAddressBookData; - $this->targetAddressBookId = $targetAddressBookId; - $this->targetAddressBookData = $targetAddressBookData; - $this->sourceShares = $sourceShares; - $this->targetShares = $targetShares; - $this->objectData = $objectData; } /** diff --git a/apps/dav/lib/Events/CardUpdatedEvent.php b/apps/dav/lib/Events/CardUpdatedEvent.php index 40f28713ce6..10fc5b74594 100644 --- a/apps/dav/lib/Events/CardUpdatedEvent.php +++ b/apps/dav/lib/Events/CardUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class CardUpdatedEvent extends Event { - /** @var int */ - private $addressBookId; - - /** @var array */ - private $addressBookData; - - /** @var array */ - private $shares; - - /** @var array */ - private $cardData; - /** * CardUpdatedEvent constructor. * @@ -56,15 +27,13 @@ class CardUpdatedEvent extends Event { * @param array $cardData * @since 20.0.0 */ - public function __construct(int $addressBookId, - array $addressBookData, - array $shares, - array $cardData) { + public function __construct( + private int $addressBookId, + private array $addressBookData, + private array $shares, + private array $cardData, + ) { parent::__construct(); - $this->addressBookId = $addressBookId; - $this->addressBookData = $addressBookData; - $this->shares = $shares; - $this->cardData = $cardData; } /** diff --git a/apps/dav/lib/Events/SabrePluginAddEvent.php b/apps/dav/lib/Events/SabrePluginAddEvent.php index 3bff756e2a1..866fae2e224 100644 --- a/apps/dav/lib/Events/SabrePluginAddEvent.php +++ b/apps/dav/lib/Events/SabrePluginAddEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -36,15 +19,13 @@ use Sabre\DAV\Server; */ class SabrePluginAddEvent extends Event { - /** @var Server */ - private $server; - /** * @since 28.0.0 */ - public function __construct(Server $server) { + public function __construct( + private Server $server, + ) { parent::__construct(); - $this->server = $server; } /** diff --git a/apps/dav/lib/Events/SabrePluginAuthInitEvent.php b/apps/dav/lib/Events/SabrePluginAuthInitEvent.php index ea1bd95a0f7..d3d93770c74 100644 --- a/apps/dav/lib/Events/SabrePluginAuthInitEvent.php +++ b/apps/dav/lib/Events/SabrePluginAuthInitEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de> - * - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -36,14 +19,12 @@ use Sabre\DAV\Server; */ class SabrePluginAuthInitEvent extends Event { - /** @var Server */ - private $server; - /** * @since 20.0.0 */ - public function __construct(Server $server) { - $this->server = $server; + public function __construct( + private Server $server, + ) { } /** diff --git a/apps/dav/lib/Events/SubscriptionCreatedEvent.php b/apps/dav/lib/Events/SubscriptionCreatedEvent.php index 9932e70c4c8..433b6db59b0 100644 --- a/apps/dav/lib/Events/SubscriptionCreatedEvent.php +++ b/apps/dav/lib/Events/SubscriptionCreatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event; */ class SubscriptionCreatedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - /** * SubscriptionCreatedEvent constructor. * @@ -48,11 +25,11 @@ class SubscriptionCreatedEvent extends Event { * @param array $subscriptionData * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; } /** diff --git a/apps/dav/lib/Events/SubscriptionDeletedEvent.php b/apps/dav/lib/Events/SubscriptionDeletedEvent.php index 8aa36f4584b..58d47ae98a4 100644 --- a/apps/dav/lib/Events/SubscriptionDeletedEvent.php +++ b/apps/dav/lib/Events/SubscriptionDeletedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event; */ class SubscriptionDeletedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - - /** @var array */ - private $shares; - /** * SubscriptionDeletedEvent constructor. * @@ -52,13 +26,12 @@ class SubscriptionDeletedEvent extends Event { * @param array $shares * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData, - array $shares) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + private array $shares, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; - $this->shares = $shares; } /** diff --git a/apps/dav/lib/Events/SubscriptionUpdatedEvent.php b/apps/dav/lib/Events/SubscriptionUpdatedEvent.php index eb90177a00d..1a1b2804663 100644 --- a/apps/dav/lib/Events/SubscriptionUpdatedEvent.php +++ b/apps/dav/lib/Events/SubscriptionUpdatedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Events; @@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event; */ class SubscriptionUpdatedEvent extends Event { - /** @var int */ - private $subscriptionId; - - /** @var array */ - private $subscriptionData; - - /** @var array */ - private $shares; - - /** @var array */ - private $mutations; - /** * SubscriptionUpdatedEvent constructor. * @@ -56,15 +27,13 @@ class SubscriptionUpdatedEvent extends Event { * @param array $mutations * @since 20.0.0 */ - public function __construct(int $subscriptionId, - array $subscriptionData, - array $shares, - array $mutations) { + public function __construct( + private int $subscriptionId, + private array $subscriptionData, + private array $shares, + private array $mutations, + ) { parent::__construct(); - $this->subscriptionId = $subscriptionId; - $this->subscriptionData = $subscriptionData; - $this->shares = $shares; - $this->mutations = $mutations; } /** diff --git a/apps/dav/lib/ExampleContentFiles/exampleContact.vcf b/apps/dav/lib/ExampleContentFiles/exampleContact.vcf new file mode 100644 index 00000000000..c58c949d0db --- /dev/null +++ b/apps/dav/lib/ExampleContentFiles/exampleContact.vcf @@ -0,0 +1,3555 @@ +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.5.6//EN +UID:cffff367-4580-4e01-8b6d-f91e95ce7e92 +FN:Leon Green +ADR;TYPE=HOME:;;123 Street Street;City;State;;Country +EMAIL;TYPE=HOME:leon@example.com +TEL;TYPE=HOME,VOICE:+999999999999 +PHOTO;ENCODING=b;TYPE=PNG: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 + ORK5CYII= +PHOTO;VALUE=URI: +PHOTO;VALUE=URI: +TITLE:Manager +ORG:Company +BDAY;VALUE=DATE:20000101 +URL;VALUE=URI:www.nextcloud.com +REV;VALUE=DATE-AND-OR-TIME:20250108T160752Z +END:VCARD diff --git a/apps/dav/lib/Exception/ExampleEventException.php b/apps/dav/lib/Exception/ExampleEventException.php new file mode 100644 index 00000000000..2d77cc443cb --- /dev/null +++ b/apps/dav/lib/Exception/ExampleEventException.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Exception; + +class ExampleEventException extends \Exception { +} diff --git a/apps/dav/lib/Exception/ServerMaintenanceMode.php b/apps/dav/lib/Exception/ServerMaintenanceMode.php index 9dad9f2d4d1..8f621588fdc 100644 --- a/apps/dav/lib/Exception/ServerMaintenanceMode.php +++ b/apps/dav/lib/Exception/ServerMaintenanceMode.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Exception; diff --git a/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php index 255a06578ac..c6b7f8564c5 100644 --- a/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php +++ b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2019 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Exception; diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php index eccae8afdd5..85ed975a409 100644 --- a/apps/dav/lib/Files/BrowserErrorPagePlugin.php +++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php @@ -1,33 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @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\Files; use OC\AppFramework\Http\Request; -use OC_Template; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; +use OCP\Security\Bruteforce\MaxDelayReached; +use OCP\Template\ITemplateManager; use Sabre\DAV\Exception; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -76,6 +61,9 @@ class BrowserErrorPagePlugin extends ServerPlugin { if ($ex instanceof Exception) { $httpCode = $ex->getHTTPCode(); $headers = $ex->getHTTPHeaders($this->server); + } elseif ($ex instanceof MaxDelayReached) { + $httpCode = 429; + $headers = []; } else { $httpCode = 500; $headers = []; @@ -94,14 +82,14 @@ class BrowserErrorPagePlugin extends ServerPlugin { * @return bool|string */ public function generateBody(int $httpCode) { - $request = \OC::$server->getRequest(); + $request = \OCP\Server::get(IRequest::class); $templateName = 'exception'; - if ($httpCode === 403 || $httpCode === 404) { + if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) { $templateName = (string)$httpCode; } - $content = new OC_Template('core', $templateName, 'guest'); + $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST); $content->assign('title', $this->server->httpResponse->getStatusText()); $content->assign('remoteAddr', $request->getRemoteAddress()); $content->assign('requestID', $request->getId()); diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 8b87ae624ba..eb548bbd55c 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @author Christian <16852529+cviereck@users.noreply.github.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Files; @@ -34,7 +14,9 @@ use OC\Files\Storage\Wrapper\Jail; use OC\Files\View; use OCA\DAV\Connector\Sabre\CachingTree; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\Server; use OCA\DAV\Connector\Sabre\TagsPlugin; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Folder; @@ -64,6 +46,7 @@ class FileSearchBackend implements ISearchBackend { public const OPERATOR_LIMIT = 100; public function __construct( + private Server $server, private CachingTree $tree, private IUser $user, private IRootFolder $rootFolder, @@ -153,6 +136,7 @@ class FileSearchBackend implements ISearchBackend { * @param string[] $requestProperties */ public function preloadPropertyFor(array $nodes, array $requestProperties): void { + $this->server->emit('preloadProperties', [$nodes, $requestProperties]); } private function getFolderForPath(?string $path = null): Folder { @@ -227,9 +211,9 @@ class FileSearchBackend implements ISearchBackend { /** @var SearchResult[] $nodes */ $nodes = array_map(function (Node $node) { if ($node instanceof Folder) { - $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager); + $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager); } else { - $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager); + $davNode = new File($this->view, $node, $this->shareManager); } $path = $this->getHrefForNode($node); $this->tree->cacheNode($davNode, $path); @@ -442,10 +426,16 @@ class FileSearchBackend implements ISearchBackend { $field = $this->mapPropertyNameToColumn($property); } + try { + $castedValue = $this->castValue($property, $value ?? ''); + } catch (\Error $e) { + throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e); + } + return new SearchComparison( $trimmedType, $field, - $this->castValue($property, $value ?? ''), + $castedValue, $extra ?? '' ); diff --git a/apps/dav/lib/Files/FilesHome.php b/apps/dav/lib/Files/FilesHome.php index 0a781b5589d..f8aa82cdcc9 100644 --- a/apps/dav/lib/Files/FilesHome.php +++ b/apps/dav/lib/Files/FilesHome.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Files; +use OC\Files\Filesystem; use OCA\DAV\Connector\Sabre\Directory; use OCP\Files\FileInfo; use Sabre\DAV\Exception\Forbidden; @@ -32,19 +15,16 @@ use Sabre\DAV\Exception\Forbidden; class FilesHome extends Directory { /** - * @var array - */ - private $principalInfo; - - /** * FilesHome constructor. * * @param array $principalInfo * @param FileInfo $userFolder */ - public function __construct($principalInfo, FileInfo $userFolder) { - $this->principalInfo = $principalInfo; - $view = \OC\Files\Filesystem::getView(); + public function __construct( + private $principalInfo, + FileInfo $userFolder, + ) { + $view = Filesystem::getView(); parent::__construct($view, $userFolder); } diff --git a/apps/dav/lib/Files/LazySearchBackend.php b/apps/dav/lib/Files/LazySearchBackend.php index ccd6fde14e1..6ba539ddd87 100644 --- a/apps/dav/lib/Files/LazySearchBackend.php +++ b/apps/dav/lib/Files/LazySearchBackend.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Files; diff --git a/apps/dav/lib/Files/RootCollection.php b/apps/dav/lib/Files/RootCollection.php index 15498ec26ec..a11bea72c59 100644 --- a/apps/dav/lib/Files/RootCollection.php +++ b/apps/dav/lib/Files/RootCollection.php @@ -1,31 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Files; use OCP\Files\FileInfo; +use OCP\IUserSession; +use OCP\Server; use Sabre\DAV\INode; use Sabre\DAV\SimpleCollection; use Sabre\DAVACL\AbstractPrincipalCollection; @@ -44,7 +28,7 @@ class RootCollection extends AbstractPrincipalCollection { */ public function getChildForPrincipal(array $principalInfo) { [,$name] = \Sabre\Uri\split($principalInfo['uri']); - $user = \OC::$server->getUserSession()->getUser(); + $user = Server::get(IUserSession::class)->getUser(); if (is_null($user) || $name !== $user->getUID()) { // a user is only allowed to see their own home contents, so in case another collection // is accessed, we return a simple empty collection for now diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php index 3ac541bbfd9..a3dbd32ce6b 100644 --- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -1,30 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Files\Sharing; -use OC\Files\View; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\Share\IShare; +use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -35,51 +20,180 @@ use Sabre\HTTP\ResponseInterface; */ class FilesDropPlugin extends ServerPlugin { - /** @var View */ - private $view; - - /** @var bool */ - private $enabled = false; + private ?IShare $share = null; + private bool $enabled = false; - /** - * @param View $view - */ - public function setView($view) { - $this->view = $view; + public function setShare(IShare $share): void { + $this->share = $share; } - public function enable() { + public function enable(): void { $this->enabled = true; } - /** * This initializes the plugin. - * - * @param \Sabre\DAV\Server $server Sabre server - * - * @return void - * @throws MethodNotAllowed + * It is ONLY initialized by the server on a file drop request. */ - public function initialize(\Sabre\DAV\Server $server) { + public function initialize(\Sabre\DAV\Server $server): void { $server->on('beforeMethod:*', [$this, 'beforeMethod'], 999); + $server->on('method:MKCOL', [$this, 'onMkcol']); $this->enabled = false; } + public function onMkcol(RequestInterface $request, ResponseInterface $response) { + if (!$this->enabled || $this->share === null) { + return; + } + + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { + return; + } + + // If this is a folder creation request we need + // to fake a success so we can pretend every + // folder now exists. + $response->setStatus(201); + return false; + } + public function beforeMethod(RequestInterface $request, ResponseInterface $response) { - if (!$this->enabled) { + if (!$this->enabled || $this->share === null) { return; } + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { + return; + } + + // Retrieve the nickname from the request + $nickname = $request->hasHeader('X-NC-Nickname') + ? trim(urldecode($request->getHeader('X-NC-Nickname'))) + : null; + if ($request->getMethod() !== 'PUT') { - throw new MethodNotAllowed('Only PUT is allowed on files drop'); + // If uploading subfolders we need to ensure they get created + // within the nickname folder + if ($request->getMethod() === 'MKCOL') { + if (!$nickname) { + throw new BadRequest('A nickname header is required when uploading subfolders'); + } + } else { + throw new MethodNotAllowed('Only PUT is allowed on files drop'); + } + } + + // If this is a folder creation request + // let's stop there and let the onMkcol handle it + if ($request->getMethod() === 'MKCOL') { + return; + } + + // Now if we create a file, we need to create the + // full path along the way. We'll only handle conflict + // resolution on file conflicts, but not on folders. + + // e.g files/dCP8yn3N86EK9sL/Folder/image.jpg + $path = $request->getPath(); + $token = $this->share->getToken(); + + // e.g files/dCP8yn3N86EK9sL + $rootPath = substr($path, 0, strpos($path, $token) + strlen($token)); + // e.g /Folder/image.jpg + $relativePath = substr($path, strlen($rootPath)); + $isRootUpload = substr_count($relativePath, '/') === 1; + + // Extract the attributes for the file request + $isFileRequest = false; + $attributes = $this->share->getAttributes(); + if ($attributes !== null) { + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } + + // We need a valid nickname for file requests + if ($isFileRequest && !$nickname) { + throw new BadRequest('A nickname header is required for file requests'); + } + + // We're only allowing the upload of + // long path with subfolders if a nickname is set. + // This prevents confusion when uploading files and help + // classify them by uploaders. + if (!$nickname && !$isRootUpload) { + throw new BadRequest('A nickname header is required when uploading subfolders'); } - $path = explode('/', $request->getPath()); - $path = array_pop($path); + if ($nickname) { + try { + $node->verifyPath($nickname); + } catch (\Exception $e) { + // If the path is not valid, we throw an exception + throw new BadRequest('Invalid nickname: ' . $nickname); + } - $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view); - $url = $request->getBaseUrl() . $newName; + // Forbid nicknames starting with a dot + if (str_starts_with($nickname, '.')) { + throw new BadRequest('Invalid nickname: ' . $nickname); + } + + // If we have a nickname, let's put + // all files in the subfolder + $relativePath = '/' . $nickname . '/' . $relativePath; + $relativePath = str_replace('//', '/', $relativePath); + } + + // Create the folders along the way + $folder = $node; + $pathSegments = $this->getPathSegments(dirname($relativePath)); + foreach ($pathSegments as $pathSegment) { + if ($pathSegment === '') { + continue; + } + + try { + // get the current folder + $currentFolder = $folder->get($pathSegment); + // check target is a folder + if ($currentFolder instanceof Folder) { + $folder = $currentFolder; + } else { + // otherwise look in the parent folder if we already create an unique folder name + foreach ($folder->getDirectoryListing() as $child) { + // we look for folders which match "NAME (SUFFIX)" + if ($child instanceof Folder && str_starts_with($child->getName(), $pathSegment)) { + $suffix = substr($child->getName(), strlen($pathSegment)); + if (preg_match('/^ \(\d+\)$/', $suffix)) { + // we found the unique folder name and can use it + $folder = $child; + break; + } + } + } + // no folder found so we need to create a new unique folder name + if (!isset($child) || $child !== $folder) { + $folder = $folder->newFolder($folder->getNonExistingName($pathSegment)); + } + } + } catch (NotFoundException) { + // the folder does simply not exist so we create it + $folder = $folder->newFolder($pathSegment); + } + } + + // Finally handle conflicts on the end files + $uniqueName = $folder->getNonExistingName(basename($relativePath)); + $relativePath = substr($folder->getPath(), strlen($node->getPath())); + $path = '/files/' . $token . '/' . $relativePath . '/' . $uniqueName; + $url = rtrim($request->getBaseUrl(), '/') . str_replace('//', '/', $path); $request->setUrl($url); } + + private function getPathSegments(string $path): array { + // Normalize slashes and remove trailing slash + $path = trim(str_replace('\\', '/', $path), '/'); + + return explode('/', $path); + } } diff --git a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php index 94cd6d29c6c..38a45b3fc37 100644 --- a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php +++ b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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\Files\Sharing; @@ -57,7 +41,7 @@ class PublicLinkCheckPlugin extends ServerPlugin { } public function beforeMethod(RequestInterface $request, ResponseInterface $response) { - // verify that the owner didn't have his share permissions revoked + // verify that the owner didn't have their share permissions revoked if ($this->fileInfo && !$this->fileInfo->isShareable()) { throw new NotFound(); } diff --git a/apps/dav/lib/Files/Sharing/RootCollection.php b/apps/dav/lib/Files/Sharing/RootCollection.php new file mode 100644 index 00000000000..dd585fbb59b --- /dev/null +++ b/apps/dav/lib/Files/Sharing/RootCollection.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Files\Sharing; + +use Sabre\DAV\INode; +use Sabre\DAVACL\AbstractPrincipalCollection; +use Sabre\DAVACL\PrincipalBackend\BackendInterface; + +class RootCollection extends AbstractPrincipalCollection { + public function __construct( + private INode $root, + BackendInterface $principalBackend, + string $principalPrefix = 'principals', + ) { + parent::__construct($principalBackend, $principalPrefix); + } + + public function getChildForPrincipal(array $principalInfo): INode { + return $this->root; + } + + public function getName() { + return 'files'; + } +} diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php deleted file mode 100644 index 46cf9621f47..00000000000 --- a/apps/dav/lib/HookManager.php +++ /dev/null @@ -1,194 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Thomas Citharel <nextcloud@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/> - * - */ -namespace OCA\DAV; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\CardDAV\SyncService; -use OCP\Defaults; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Util; -use Psr\Log\LoggerInterface; - -class HookManager { - - /** @var IUserManager */ - private $userManager; - - /** @var SyncService */ - private $syncService; - - /** @var IUser[] */ - private $usersToDelete = []; - - /** @var CalDavBackend */ - private $calDav; - - /** @var CardDavBackend */ - private $cardDav; - - /** @var array */ - private $calendarsToDelete = []; - - /** @var array */ - private $subscriptionsToDelete = []; - - /** @var array */ - private $addressBooksToDelete = []; - - /** @var Defaults */ - private $themingDefaults; - - public function __construct(IUserManager $userManager, - SyncService $syncService, - CalDavBackend $calDav, - CardDavBackend $cardDav, - Defaults $themingDefaults) { - $this->userManager = $userManager; - $this->syncService = $syncService; - $this->calDav = $calDav; - $this->cardDav = $cardDav; - $this->themingDefaults = $themingDefaults; - } - - public function setup() { - Util::connectHook('OC_User', - 'post_createUser', - $this, - 'postCreateUser'); - \OC::$server->getUserManager()->listen('\OC\User', 'assignedUserId', function ($uid) { - $this->postCreateUser(['uid' => $uid]); - }); - Util::connectHook('OC_User', - 'pre_deleteUser', - $this, - 'preDeleteUser'); - \OC::$server->getUserManager()->listen('\OC\User', 'preUnassignedUserId', [$this, 'preUnassignedUserId']); - Util::connectHook('OC_User', - 'post_deleteUser', - $this, - 'postDeleteUser'); - \OC::$server->getUserManager()->listen('\OC\User', 'postUnassignedUserId', function ($uid) { - $this->postDeleteUser(['uid' => $uid]); - }); - \OC::$server->getUserManager()->listen('\OC\User', 'postUnassignedUserId', [$this, 'postUnassignedUserId']); - Util::connectHook('OC_User', 'changeUser', $this, 'changeUser'); - } - - public function postCreateUser($params) { - $user = $this->userManager->get($params['uid']); - if ($user instanceof IUser) { - $this->syncService->updateUser($user); - } - } - - public function preDeleteUser($params) { - $uid = $params['uid']; - $userPrincipalUri = 'principals/users/' . $uid; - $this->usersToDelete[$uid] = $this->userManager->get($uid); - $this->calendarsToDelete = $this->calDav->getUsersOwnCalendars($userPrincipalUri); - $this->subscriptionsToDelete = $this->calDav->getSubscriptionsForUser($userPrincipalUri); - $this->addressBooksToDelete = $this->cardDav->getUsersOwnAddressBooks($userPrincipalUri); - } - - public function preUnassignedUserId($uid) { - $this->usersToDelete[$uid] = $this->userManager->get($uid); - } - - public function postDeleteUser($params) { - $uid = $params['uid']; - if (isset($this->usersToDelete[$uid])) { - $this->syncService->deleteUser($this->usersToDelete[$uid]); - } - - foreach ($this->calendarsToDelete as $calendar) { - $this->calDav->deleteCalendar( - $calendar['id'], - true // Make sure the data doesn't go into the trashbin, a new user with the same UID would later see it otherwise - ); - } - - foreach ($this->subscriptionsToDelete as $subscription) { - $this->calDav->deleteSubscription( - $subscription['id'], - ); - } - $this->calDav->deleteAllSharesByUser('principals/users/' . $uid); - - foreach ($this->addressBooksToDelete as $addressBook) { - $this->cardDav->deleteAddressBook($addressBook['id']); - } - } - - public function postUnassignedUserId($uid) { - if (isset($this->usersToDelete[$uid])) { - $this->syncService->deleteUser($this->usersToDelete[$uid]); - } - } - - public function changeUser($params) { - $user = $params['user']; - $feature = $params['feature']; - // This case is already covered by the account manager firing up a signal - // later on - if ($feature !== 'eMailAddress' && $feature !== 'displayName') { - $this->syncService->updateUser($user); - } - } - - /** - * @return void - */ - public function firstLogin(?IUser $user = null) { - if (!is_null($user)) { - $principal = 'principals/users/' . $user->getUID(); - if ($this->calDav->getCalendarsForUserCount($principal) === 0) { - try { - $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [ - '{DAV:}displayname' => CalDavBackend::PERSONAL_CALENDAR_NAME, - '{http://apple.com/ns/ical/}calendar-color' => $this->themingDefaults->getColorPrimary(), - 'components' => 'VEVENT' - ]); - } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); - } - } - if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) { - try { - $this->cardDav->createAddressBook($principal, CardDavBackend::PERSONAL_ADDRESSBOOK_URI, [ - '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME, - ]); - } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); - } - } - } - } -} diff --git a/apps/dav/lib/Listener/ActivityUpdaterListener.php b/apps/dav/lib/Listener/ActivityUpdaterListener.php index 3f958965f06..f291e424c41 100644 --- a/apps/dav/lib/Listener/ActivityUpdaterListener.php +++ b/apps/dav/lib/Listener/ActivityUpdaterListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -30,14 +13,14 @@ use OCA\DAV\DAV\Sharing\Plugin; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectCreatedEvent; -use OCA\DAV\Events\CalendarObjectDeletedEvent; -use OCA\DAV\Events\CalendarObjectMovedEvent; -use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectRestoredEvent; -use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarUpdatedEvent; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use OCP\Calendar\Events\CalendarObjectMovedEvent; +use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent; +use OCP\Calendar\Events\CalendarObjectRestoredEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use Psr\Log\LoggerInterface; @@ -47,16 +30,10 @@ use function sprintf; /** @template-implements IEventListener<CalendarCreatedEvent|CalendarUpdatedEvent|CalendarMovedToTrashEvent|CalendarRestoredEvent|CalendarDeletedEvent|CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectMovedEvent|CalendarObjectMovedToTrashEvent|CalendarObjectRestoredEvent|CalendarObjectDeletedEvent> */ class ActivityUpdaterListener implements IEventListener { - /** @var ActivityBackend */ - private $activityBackend; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(ActivityBackend $activityBackend, - LoggerInterface $logger) { - $this->activityBackend = $activityBackend; - $this->logger = $logger; + public function __construct( + private ActivityBackend $activityBackend, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/AddMissingIndicesListener.php b/apps/dav/lib/Listener/AddMissingIndicesListener.php new file mode 100644 index 00000000000..d3a1cf4b224 --- /dev/null +++ b/apps/dav/lib/Listener/AddMissingIndicesListener.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Listener; + +use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +/** + * @template-implements IEventListener<Event|AddMissingIndicesEvent> + */ +class AddMissingIndicesListener implements IEventListener { + + public function handle(Event $event): void { + if (!($event instanceof AddMissingIndicesEvent)) { + return; + } + $event->addMissingIndex( + 'dav_shares', + 'dav_shares_resourceid_type', + ['resourceid', 'type'] + ); + $event->addMissingIndex( + 'dav_shares', + 'dav_shares_resourceid_access', + ['resourceid', 'access'] + ); + $event->addMissingIndex( + 'calendarobjects', + 'calobjects_by_uid_index', + ['calendarid', 'calendartype', 'uid'] + ); + } + +} diff --git a/apps/dav/lib/Listener/AddressbookListener.php b/apps/dav/lib/Listener/AddressbookListener.php index 6c348a25b59..4e38ce50dfd 100644 --- a/apps/dav/lib/Listener/AddressbookListener.php +++ b/apps/dav/lib/Listener/AddressbookListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -38,16 +21,10 @@ use function sprintf; /** @template-implements IEventListener<AddressBookCreatedEvent|AddressBookUpdatedEvent|AddressBookDeletedEvent|AddressBookShareUpdatedEvent> */ class AddressbookListener implements IEventListener { - /** @var ActivityBackend */ - private $activityBackend; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(ActivityBackend $activityBackend, - LoggerInterface $logger) { - $this->activityBackend = $activityBackend; - $this->logger = $logger; + public function __construct( + private ActivityBackend $activityBackend, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/BirthdayListener.php b/apps/dav/lib/Listener/BirthdayListener.php index a315cfcc74c..3a464d668f9 100644 --- a/apps/dav/lib/Listener/BirthdayListener.php +++ b/apps/dav/lib/Listener/BirthdayListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -34,10 +17,9 @@ use OCP\EventDispatcher\IEventListener; /** @template-implements IEventListener<CardCreatedEvent|CardUpdatedEvent|CardDeletedEvent> */ class BirthdayListener implements IEventListener { - private BirthdayService $birthdayService; - - public function __construct(BirthdayService $birthdayService) { - $this->birthdayService = $birthdayService; + public function __construct( + private BirthdayService $birthdayService, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/CalendarContactInteractionListener.php b/apps/dav/lib/Listener/CalendarContactInteractionListener.php index 5e23891de3d..a7f00e452c4 100644 --- a/apps/dav/lib/Listener/CalendarContactInteractionListener.php +++ b/apps/dav/lib/Listener/CalendarContactInteractionListener.php @@ -3,32 +3,15 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; use OCA\DAV\Connector\Sabre\Principal; -use OCA\DAV\Events\CalendarObjectCreatedEvent; -use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\Contacts\Events\ContactInteractedWithEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -49,31 +32,13 @@ use function substr; class CalendarContactInteractionListener implements IEventListener { private const URI_USERS = 'principals/users/'; - /** @var IEventDispatcher */ - private $dispatcher; - - /** @var IUserSession */ - private $userSession; - - /** @var Principal */ - private $principalConnector; - - /** @var IMailer */ - private $mailer; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(IEventDispatcher $dispatcher, - IUserSession $userSession, - Principal $principalConnector, - IMailer $mailer, - LoggerInterface $logger) { - $this->dispatcher = $dispatcher; - $this->userSession = $userSession; - $this->principalConnector = $principalConnector; - $this->mailer = $mailer; - $this->logger = $logger; + public function __construct( + private IEventDispatcher $dispatcher, + private IUserSession $userSession, + private Principal $principalConnector, + private IMailer $mailer, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php b/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php index 39f33154603..0cfc435eb8c 100644 --- a/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php +++ b/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -37,16 +20,10 @@ use Throwable; */ class CalendarDeletionDefaultUpdaterListener implements IEventListener { - /** @var IConfig */ - private $config; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(IConfig $config, - LoggerInterface $logger) { - $this->config = $config; - $this->logger = $logger; + public function __construct( + private IConfig $config, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php index 51f31a12f8b..a58fb3524ab 100644 --- a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php +++ b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -30,12 +13,12 @@ use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; use OCA\DAV\CalDAV\Reminder\ReminderService; use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectCreatedEvent; -use OCA\DAV\Events\CalendarObjectDeletedEvent; -use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; -use OCA\DAV\Events\CalendarObjectRestoredEvent; -use OCA\DAV\Events\CalendarObjectUpdatedEvent; use OCA\DAV\Events\CalendarRestoredEvent; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent; +use OCP\Calendar\Events\CalendarObjectRestoredEvent; +use OCP\Calendar\Events\CalendarObjectUpdatedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use Psr\Log\LoggerInterface; @@ -45,26 +28,12 @@ use function sprintf; /** @template-implements IEventListener<CalendarMovedToTrashEvent|CalendarDeletedEvent|CalendarRestoredEvent|CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectMovedToTrashEvent|CalendarObjectRestoredEvent|CalendarObjectDeletedEvent> */ class CalendarObjectReminderUpdaterListener implements IEventListener { - /** @var ReminderBackend */ - private $reminderBackend; - - /** @var ReminderService */ - private $reminderService; - - /** @var CalDavBackend */ - private $calDavBackend; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(ReminderBackend $reminderBackend, - ReminderService $reminderService, - CalDavBackend $calDavBackend, - LoggerInterface $logger) { - $this->reminderBackend = $reminderBackend; - $this->reminderService = $reminderService; - $this->calDavBackend = $calDavBackend; - $this->logger = $logger; + public function __construct( + private ReminderBackend $reminderBackend, + private ReminderService $reminderService, + private CalDavBackend $calDavBackend, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/CalendarPublicationListener.php b/apps/dav/lib/Listener/CalendarPublicationListener.php index 7ff419324a9..94a0a208d4e 100644 --- a/apps/dav/lib/Listener/CalendarPublicationListener.php +++ b/apps/dav/lib/Listener/CalendarPublicationListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -34,13 +17,10 @@ use Psr\Log\LoggerInterface; /** @template-implements IEventListener<CalendarPublishedEvent|CalendarUnpublishedEvent> */ class CalendarPublicationListener implements IEventListener { - private Backend $activityBackend; - private LoggerInterface $logger; - - public function __construct(Backend $activityBackend, - LoggerInterface $logger) { - $this->activityBackend = $activityBackend; - $this->logger = $logger; + public function __construct( + private Backend $activityBackend, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/Listener/CalendarShareUpdateListener.php b/apps/dav/lib/Listener/CalendarShareUpdateListener.php index 53c4a9d4438..b673d5d2e42 100644 --- a/apps/dav/lib/Listener/CalendarShareUpdateListener.php +++ b/apps/dav/lib/Listener/CalendarShareUpdateListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -33,13 +16,10 @@ use Psr\Log\LoggerInterface; /** @template-implements IEventListener<CalendarShareUpdatedEvent> */ class CalendarShareUpdateListener implements IEventListener { - private Backend $activityBackend; - private LoggerInterface $logger; - - public function __construct(Backend $activityBackend, - LoggerInterface $logger) { - $this->activityBackend = $activityBackend; - $this->logger = $logger; + public function __construct( + private Backend $activityBackend, + private LoggerInterface $logger, + ) { } /** @@ -51,7 +31,7 @@ class CalendarShareUpdateListener implements IEventListener { return; } - $this->logger->debug("Creating activity for Calendar having its shares updated"); + $this->logger->debug('Creating activity for Calendar having its shares updated'); $this->activityBackend->onCalendarUpdateShares( $event->getCalendarData(), @@ -59,5 +39,7 @@ class CalendarShareUpdateListener implements IEventListener { $event->getAdded(), $event->getRemoved() ); + + // Here we should recalculate if reminders should be sent to new or old sharees } } diff --git a/apps/dav/lib/Listener/CardListener.php b/apps/dav/lib/Listener/CardListener.php index 57acde4bd8e..b9fd1a7f64b 100644 --- a/apps/dav/lib/Listener/CardListener.php +++ b/apps/dav/lib/Listener/CardListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -38,16 +21,10 @@ use function sprintf; /** @template-implements IEventListener<CardCreatedEvent|CardUpdatedEvent|CardDeletedEvent> */ class CardListener implements IEventListener { - /** @var ActivityBackend */ - private $activityBackend; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(ActivityBackend $activityBackend, - LoggerInterface $logger) { - $this->activityBackend = $activityBackend; - $this->logger = $logger; + public function __construct( + private ActivityBackend $activityBackend, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/ClearPhotoCacheListener.php b/apps/dav/lib/Listener/ClearPhotoCacheListener.php index 605e54aa3bc..eb599d33871 100644 --- a/apps/dav/lib/Listener/ClearPhotoCacheListener.php +++ b/apps/dav/lib/Listener/ClearPhotoCacheListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -33,10 +16,9 @@ use OCP\EventDispatcher\IEventListener; /** @template-implements IEventListener<CardUpdatedEvent|CardDeletedEvent> */ class ClearPhotoCacheListener implements IEventListener { - private PhotoCache $photoCache; - - public function __construct(PhotoCache $photoCache) { - $this->photoCache = $photoCache; + public function __construct( + private PhotoCache $photoCache, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/DavAdminSettingsListener.php b/apps/dav/lib/Listener/DavAdminSettingsListener.php new file mode 100644 index 00000000000..69501915208 --- /dev/null +++ b/apps/dav/lib/Listener/DavAdminSettingsListener.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Listener; + +use OCA\DAV\AppInfo\Application; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IAppConfig; +use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; +use OCP\Settings\Events\DeclarativeSettingsSetValueEvent; + +/** @template-implements IEventListener<DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent> */ +class DavAdminSettingsListener implements IEventListener { + + public function __construct( + private IAppConfig $config, + ) { + } + + public function handle(Event $event): void { + + /** @var DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent $event */ + if ($event->getApp() !== Application::APP_ID) { + return; + } + + if ($event->getFormId() !== 'dav-admin-system-address-book') { + return; + } + + if ($event instanceof DeclarativeSettingsGetValueEvent) { + $this->handleGetValue($event); + return; + } + + if ($event instanceof DeclarativeSettingsSetValueEvent) { + $this->handleSetValue($event); + return; + } + + } + + private function handleGetValue(DeclarativeSettingsGetValueEvent $event): void { + + if ($event->getFieldId() === 'system_addressbook_enabled') { + $event->setValue((int)$this->config->getValueBool('dav', 'system_addressbook_exposed', true)); + } + + } + + private function handleSetValue(DeclarativeSettingsSetValueEvent $event): void { + + if ($event->getFieldId() === 'system_addressbook_enabled') { + $this->config->setValueBool('dav', 'system_addressbook_exposed', (bool)$event->getValue()); + $event->stopPropagation(); + } + + } + +} diff --git a/apps/dav/lib/Listener/OutOfOfficeListener.php b/apps/dav/lib/Listener/OutOfOfficeListener.php index 6adc42ceeba..45728aa35d3 100644 --- a/apps/dav/lib/Listener/OutOfOfficeListener.php +++ b/apps/dav/lib/Listener/OutOfOfficeListener.php @@ -3,24 +3,8 @@ declare(strict_types=1); /** - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -55,7 +39,7 @@ class OutOfOfficeListener implements IEventListener { private ServerFactory $serverFactory, private IConfig $appConfig, private TimezoneService $timezoneService, - private LoggerInterface $logger + private LoggerInterface $logger, ) { } diff --git a/apps/dav/lib/Listener/SubscriptionListener.php b/apps/dav/lib/Listener/SubscriptionListener.php index 645a33e690d..fc9dfcf122d 100644 --- a/apps/dav/lib/Listener/SubscriptionListener.php +++ b/apps/dav/lib/Listener/SubscriptionListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -37,17 +20,12 @@ use Psr\Log\LoggerInterface; /** @template-implements IEventListener<SubscriptionCreatedEvent|SubscriptionDeletedEvent> */ class SubscriptionListener implements IEventListener { - private IJobList $jobList; - private RefreshWebcalService $refreshWebcalService; - private ReminderBackend $reminderBackend; - private LoggerInterface $logger; - - public function __construct(IJobList $jobList, RefreshWebcalService $refreshWebcalService, ReminderBackend $reminderBackend, - LoggerInterface $logger) { - $this->jobList = $jobList; - $this->refreshWebcalService = $refreshWebcalService; - $this->reminderBackend = $reminderBackend; - $this->logger = $logger; + public function __construct( + private IJobList $jobList, + private RefreshWebcalService $refreshWebcalService, + private ReminderBackend $reminderBackend, + private LoggerInterface $logger, + ) { } /** diff --git a/apps/dav/lib/Listener/TrustedServerRemovedListener.php b/apps/dav/lib/Listener/TrustedServerRemovedListener.php index 7be84a5b779..9adbcfc14c2 100644 --- a/apps/dav/lib/Listener/TrustedServerRemovedListener.php +++ b/apps/dav/lib/Listener/TrustedServerRemovedListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Carl Schwan <carl@carlschwan.eu> - * - * @author Carl Schwan <carl@carlschwan.eu> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -32,10 +15,9 @@ use OCP\Federation\Events\TrustedServerRemovedEvent; /** @template-implements IEventListener<TrustedServerRemovedEvent> */ class TrustedServerRemovedListener implements IEventListener { - private CardDavBackend $cardDavBackend; - - public function __construct(CardDavBackend $cardDavBackend) { - $this->cardDavBackend = $cardDavBackend; + public function __construct( + private CardDavBackend $cardDavBackend, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Listener/UserEventsListener.php b/apps/dav/lib/Listener/UserEventsListener.php new file mode 100644 index 00000000000..a6b09b70fa0 --- /dev/null +++ b/apps/dav/lib/Listener/UserEventsListener.php @@ -0,0 +1,186 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\Service\ExampleContactService; +use OCA\DAV\Service\ExampleEventService; +use OCP\Accounts\UserUpdatedEvent; +use OCP\BackgroundJob\IJobList; +use OCP\Defaults; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\BeforeUserDeletedEvent; +use OCP\User\Events\BeforeUserIdUnassignedEvent; +use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserCreatedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserFirstTimeLoggedInEvent; +use OCP\User\Events\UserIdAssignedEvent; +use OCP\User\Events\UserIdUnassignedEvent; +use Psr\Log\LoggerInterface; + +/** @template-implements IEventListener<UserFirstTimeLoggedInEvent|UserIdAssignedEvent|BeforeUserIdUnassignedEvent|UserIdUnassignedEvent|BeforeUserDeletedEvent|UserDeletedEvent|UserCreatedEvent|UserChangedEvent|UserUpdatedEvent> */ +class UserEventsListener implements IEventListener { + + /** @var IUser[] */ + private array $usersToDelete = []; + + private array $calendarsToDelete = []; + private array $subscriptionsToDelete = []; + private array $addressBooksToDelete = []; + + public function __construct( + private IUserManager $userManager, + private SyncService $syncService, + private CalDavBackend $calDav, + private CardDavBackend $cardDav, + private Defaults $themingDefaults, + private ExampleContactService $exampleContactService, + private ExampleEventService $exampleEventService, + private LoggerInterface $logger, + private IJobList $jobList, + ) { + } + + public function handle(Event $event): void { + if ($event instanceof UserCreatedEvent) { + $this->postCreateUser($event->getUser()); + } elseif ($event instanceof UserIdAssignedEvent) { + $user = $this->userManager->get($event->getUserId()); + if ($user !== null) { + $this->postCreateUser($user); + } + } elseif ($event instanceof BeforeUserDeletedEvent) { + $this->preDeleteUser($event->getUser()); + } elseif ($event instanceof BeforeUserIdUnassignedEvent) { + $this->preUnassignedUserId($event->getUserId()); + } elseif ($event instanceof UserDeletedEvent) { + $this->postDeleteUser($event->getUid()); + } elseif ($event instanceof UserIdUnassignedEvent) { + $this->postDeleteUser($event->getUserId()); + } elseif ($event instanceof UserChangedEvent) { + $this->changeUser($event->getUser(), $event->getFeature()); + } elseif ($event instanceof UserFirstTimeLoggedInEvent) { + $this->firstLogin($event->getUser()); + } elseif ($event instanceof UserUpdatedEvent) { + $this->updateUser($event->getUser()); + } + } + + public function postCreateUser(IUser $user): void { + $this->syncService->updateUser($user); + } + + public function updateUser(IUser $user): void { + $this->syncService->updateUser($user); + } + + public function preDeleteUser(IUser $user): void { + $uid = $user->getUID(); + $userPrincipalUri = 'principals/users/' . $uid; + $this->usersToDelete[$uid] = $user; + $this->calendarsToDelete[$uid] = $this->calDav->getUsersOwnCalendars($userPrincipalUri); + $this->subscriptionsToDelete[$uid] = $this->calDav->getSubscriptionsForUser($userPrincipalUri); + $this->addressBooksToDelete[$uid] = $this->cardDav->getUsersOwnAddressBooks($userPrincipalUri); + } + + public function preUnassignedUserId(string $uid): void { + $user = $this->userManager->get($uid); + if ($user !== null) { + $this->usersToDelete[$uid] = $user; + } + } + + public function postDeleteUser(string $uid): void { + if (isset($this->usersToDelete[$uid])) { + $this->syncService->deleteUser($this->usersToDelete[$uid]); + } + + foreach ($this->calendarsToDelete[$uid] as $calendar) { + $this->calDav->deleteCalendar( + $calendar['id'], + true // Make sure the data doesn't go into the trashbin, a new user with the same UID would later see it otherwise + ); + } + + foreach ($this->subscriptionsToDelete[$uid] as $subscription) { + $this->calDav->deleteSubscription( + $subscription['id'], + ); + } + $this->calDav->deleteAllSharesByUser('principals/users/' . $uid); + + foreach ($this->addressBooksToDelete[$uid] as $addressBook) { + $this->cardDav->deleteAddressBook($addressBook['id']); + } + + $this->jobList->remove(UserStatusAutomation::class, ['userId' => $uid]); + + unset($this->calendarsToDelete[$uid]); + unset($this->subscriptionsToDelete[$uid]); + unset($this->addressBooksToDelete[$uid]); + } + + public function changeUser(IUser $user, string $feature): void { + // This case is already covered by the account manager firing up a signal + // later on + if ($feature !== 'eMailAddress' && $feature !== 'displayName') { + $this->syncService->updateUser($user); + } + } + + public function firstLogin(IUser $user): void { + $principal = 'principals/users/' . $user->getUID(); + + $calendarId = null; + if ($this->calDav->getCalendarsForUserCount($principal) === 0) { + try { + $calendarId = $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [ + '{DAV:}displayname' => CalDavBackend::PERSONAL_CALENDAR_NAME, + '{http://apple.com/ns/ical/}calendar-color' => $this->themingDefaults->getColorPrimary(), + 'components' => 'VEVENT' + ]); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + if ($calendarId !== null) { + try { + $this->exampleEventService->createExampleEvent($calendarId); + } catch (\Exception $e) { + $this->logger->error('Failed to create example event: ' . $e->getMessage(), [ + 'exception' => $e, + 'userId' => $user->getUID(), + 'calendarId' => $calendarId, + ]); + } + } + + $addressBookId = null; + if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) { + try { + $addressBookId = $this->cardDav->createAddressBook($principal, CardDavBackend::PERSONAL_ADDRESSBOOK_URI, [ + '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME, + ]); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + if ($addressBookId) { + $this->exampleContactService->createDefaultContact($addressBookId); + } + } +} diff --git a/apps/dav/lib/Listener/UserPreferenceListener.php b/apps/dav/lib/Listener/UserPreferenceListener.php index 885ebbc36c6..5f5fed05348 100644 --- a/apps/dav/lib/Listener/UserPreferenceListener.php +++ b/apps/dav/lib/Listener/UserPreferenceListener.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Listener; @@ -34,10 +17,9 @@ use OCP\EventDispatcher\IEventListener; /** @template-implements IEventListener<BeforePreferenceSetEvent|BeforePreferenceDeletedEvent> */ class UserPreferenceListener implements IEventListener { - protected IJobList $jobList; - - public function __construct(IJobList $jobList) { - $this->jobList = $jobList; + public function __construct( + protected IJobList $jobList, + ) { } public function handle(Event $event): void { diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php index fc9a6d9a559..d8f906f22ee 100644 --- a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -34,26 +14,11 @@ use OCP\Migration\IRepairStep; class BuildCalendarSearchIndex implements IRepairStep { - /** @var IDBConnection */ - private $db; - - /** @var IJobList */ - private $jobList; - - /** @var IConfig */ - private $config; - - /** - * @param IDBConnection $db - * @param IJobList $jobList - * @param IConfig $config - */ - public function __construct(IDBConnection $db, - IJobList $jobList, - IConfig $config) { - $this->db = $db; - $this->jobList = $jobList; - $this->config = $config; + public function __construct( + private IDBConnection $db, + private IJobList $jobList, + private IConfig $config, + ) { } /** @@ -76,8 +41,8 @@ class BuildCalendarSearchIndex implements IRepairStep { $query = $this->db->getQueryBuilder(); $query->select($query->createFunction('MAX(' . $query->getColumnName('id') . ')')) ->from('calendarobjects'); - $result = $query->execute(); - $maxId = (int) $result->fetchOne(); + $result = $query->executeQuery(); + $maxId = (int)$result->fetchOne(); $result->closeCursor(); $output->info('Add background job'); diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php index 0b89a3d1d77..da8f31e7d3d 100644 --- a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -40,16 +21,16 @@ class BuildCalendarSearchIndexBackgroundJob extends QueuedJob { private CalDavBackend $calDavBackend, private LoggerInterface $logger, private IJobList $jobList, - ITimeFactory $timeFactory + ITimeFactory $timeFactory, ) { parent::__construct($timeFactory); } public function run($arguments) { - $offset = (int) $arguments['offset']; - $stopAt = (int) $arguments['stopAt']; + $offset = (int)$arguments['offset']; + $stopAt = (int)$arguments['stopAt']; - $this->logger->info('Building calendar index (' . $offset .'/' . $stopAt . ')'); + $this->logger->info('Building calendar index (' . $offset . '/' . $stopAt . ')'); $startTime = $this->time->getTime(); while (($this->time->getTime() - $startTime) < 15) { diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndex.php b/apps/dav/lib/Migration/BuildSocialSearchIndex.php index f3872acc3ab..a808034365a 100644 --- a/apps/dav/lib/Migration/BuildSocialSearchIndex.php +++ b/apps/dav/lib/Migration/BuildSocialSearchIndex.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author call-me-matt <nextcloud@matthiasheinisch.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -31,26 +14,16 @@ use OCP\Migration\IRepairStep; class BuildSocialSearchIndex implements IRepairStep { - /** @var IDBConnection */ - private $db; - - /** @var IJobList */ - private $jobList; - - /** @var IConfig */ - private $config; - /** * @param IDBConnection $db * @param IJobList $jobList * @param IConfig $config */ - public function __construct(IDBConnection $db, - IJobList $jobList, - IConfig $config) { - $this->db = $db; - $this->jobList = $jobList; - $this->config = $config; + public function __construct( + private IDBConnection $db, + private IJobList $jobList, + private IConfig $config, + ) { } /** diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php index 9a688970597..fab61d56fd6 100644 --- a/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php +++ b/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2020 Matthias Heinisch <nextcloud@matthiasheinisch.de> - * - * @author call-me-matt <nextcloud@matthiasheinisch.de> - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -30,6 +12,7 @@ use OCA\DAV\CardDAV\CardDavBackend; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\QueuedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use Psr\Log\LoggerInterface; @@ -48,7 +31,7 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob { $offset = $arguments['offset']; $stopAt = $arguments['stopAt']; - $this->logger->info('Indexing social profile data (' . $offset .'/' . $stopAt . ')'); + $this->logger->info('Indexing social profile data (' . $offset . '/' . $stopAt . ')'); $offset = $this->buildIndex($offset, $stopAt); @@ -77,6 +60,7 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob { ->from('cards', 'c') ->orderBy('id', 'ASC') ->where($query->expr()->like('carddata', $query->createNamedParameter('%SOCIALPROFILE%'))) + ->andWhere($query->expr()->gt('id', $query->createNamedParameter((int)$offset, IQueryBuilder::PARAM_INT))) ->setMaxResults(100); $social_cards = $query->executeQuery()->fetchAll(); @@ -87,7 +71,11 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob { // refresh identified contacts in order to re-index foreach ($social_cards as $contact) { $offset = $contact['id']; - $this->davBackend->updateCard($contact['addressbookid'], $contact['uri'], $contact['carddata']); + $cardData = $contact['carddata']; + if (is_resource($cardData) && (get_resource_type($cardData) === 'stream')) { + $cardData = stream_get_contents($cardData); + } + $this->davBackend->updateCard($contact['addressbookid'], $contact['uri'], $cardData); // stop after 15sec (to be continued with next chunk) if (($this->time->getTime() - $startTime) > 15) { diff --git a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php index 9b96df2ae1e..24e182e46eb 100644 --- a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php +++ b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php @@ -1,30 +1,11 @@ <?php + /** - * @copyright 2017 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; -use Doctrine\DBAL\Platforms\OraclePlatform; use OCA\DAV\CalDAV\CalDavBackend; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -35,18 +16,11 @@ use Sabre\VObject\InvalidDataException; class CalDAVRemoveEmptyValue implements IRepairStep { - /** @var IDBConnection */ - private $db; - - /** @var CalDavBackend */ - private $calDavBackend; - - private LoggerInterface $logger; - - public function __construct(IDBConnection $db, CalDavBackend $calDavBackend, LoggerInterface $logger) { - $this->db = $db; - $this->calDavBackend = $calDavBackend; - $this->logger = $logger; + public function __construct( + private IDBConnection $db, + private CalDavBackend $calDavBackend, + private LoggerInterface $logger, + ) { } public function getName() { @@ -94,13 +68,13 @@ class CalDAVRemoveEmptyValue implements IRepairStep { } protected function getInvalidObjects($pattern) { - if ($this->db->getDatabasePlatform() instanceof OraclePlatform) { + if ($this->db->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) { $rows = []; $chunkSize = 500; $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('*', 'num_entries')) ->from('calendarobjects'); - $result = $query->execute(); + $result = $query->executeQuery(); $count = $result->fetchOne(); $result->closeCursor(); @@ -112,7 +86,7 @@ class CalDAVRemoveEmptyValue implements IRepairStep { ->setMaxResults($chunkSize); for ($chunk = 0; $chunk < $numChunks; $chunk++) { $query->setFirstResult($chunk * $chunkSize); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { if (mb_strpos($row['calendardata'], $pattern) !== false) { @@ -137,7 +111,7 @@ class CalDAVRemoveEmptyValue implements IRepairStep { IQueryBuilder::PARAM_STR )); - $result = $query->execute(); + $result = $query->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); diff --git a/apps/dav/lib/Migration/ChunkCleanup.php b/apps/dav/lib/Migration/ChunkCleanup.php index 8a4ad5664a4..edd9a26109e 100644 --- a/apps/dav/lib/Migration/ChunkCleanup.php +++ b/apps/dav/lib/Migration/ChunkCleanup.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -38,23 +21,12 @@ use OCP\Migration\IRepairStep; class ChunkCleanup implements IRepairStep { - /** @var IConfig */ - private $config; - /** @var IUserManager */ - private $userManager; - /** @var IRootFolder */ - private $rootFolder; - /** @var IJobList */ - private $jobList; - - public function __construct(IConfig $config, - IUserManager $userManager, - IRootFolder $rootFolder, - IJobList $jobList) { - $this->config = $config; - $this->userManager = $userManager; - $this->rootFolder = $rootFolder; - $this->jobList = $jobList; + public function __construct( + private IConfig $config, + private IUserManager $userManager, + private IRootFolder $rootFolder, + private IJobList $jobList, + ) { } public function getName(): string { @@ -65,11 +37,12 @@ class ChunkCleanup implements IRepairStep { // If we already ran this onec there is no need to run it again if ($this->config->getAppValue('dav', 'chunks_migrated', '0') === '1') { $output->info('Cleanup not required'); + return; } $output->startProgress(); // Loop over all seen users - $this->userManager->callForSeenUsers(function (IUser $user) use ($output) { + $this->userManager->callForSeenUsers(function (IUser $user) use ($output): void { try { $userFolder = $this->rootFolder->getUserFolder($user->getUID()); $userRoot = $userFolder->getParent(); diff --git a/apps/dav/lib/Migration/CreateSystemAddressBookStep.php b/apps/dav/lib/Migration/CreateSystemAddressBookStep.php new file mode 100644 index 00000000000..ec07c72e7a7 --- /dev/null +++ b/apps/dav/lib/Migration/CreateSystemAddressBookStep.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use OCA\DAV\CardDAV\SyncService; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class CreateSystemAddressBookStep implements IRepairStep { + + public function __construct( + private SyncService $syncService, + ) { + } + + public function getName(): string { + return 'Create system address book'; + } + + public function run(IOutput $output): void { + $this->syncService->ensureLocalSystemAddressBookExists(); + } +} diff --git a/apps/dav/lib/Migration/DeleteSchedulingObjects.php b/apps/dav/lib/Migration/DeleteSchedulingObjects.php new file mode 100644 index 00000000000..3cb3c9c9b10 --- /dev/null +++ b/apps/dav/lib/Migration/DeleteSchedulingObjects.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Migration; + +use OCA\DAV\BackgroundJob\DeleteOutdatedSchedulingObjects; +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class DeleteSchedulingObjects implements IRepairStep { + public function __construct( + private IJobList $jobList, + private ITimeFactory $time, + private CalDavBackend $calDavBackend, + ) { + } + + public function getName(): string { + return 'Handle outdated scheduling events'; + } + + public function run(IOutput $output): void { + $output->info('Cleaning up old scheduling events'); + $time = $this->time->getTime() - (60 * 60); + $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000); + if (!$this->jobList->has(DeleteOutdatedSchedulingObjects::class, null)) { + $output->info('Adding background job to delete old scheduling objects'); + $this->jobList->add(DeleteOutdatedSchedulingObjects::class, null); + } + } +} diff --git a/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php b/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php index 6aa499c8b1a..6833ca2ffa6 100644 --- a/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php +++ b/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php @@ -1,23 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @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 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Migration; @@ -28,16 +13,9 @@ use OCP\Migration\IRepairStep; class FixBirthdayCalendarComponent implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** - * FixBirthdayCalendarComponent constructor. - * - * @param IDBConnection $connection - */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } /** @@ -55,7 +33,7 @@ class FixBirthdayCalendarComponent implements IRepairStep { $updated = $query->update('calendars') ->set('components', $query->createNamedParameter('VEVENT')) ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) - ->execute(); + ->executeStatement(); $output->info("$updated birthday calendars updated."); } diff --git a/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php b/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php index ae930712859..cd4b8b31f4d 100644 --- a/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php +++ b/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -34,21 +16,16 @@ use OCP\Migration\IRepairStep; class RefreshWebcalJobRegistrar implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** @var IJobList */ - private $jobList; - /** * FixBirthdayCalendarComponent constructor. * * @param IDBConnection $connection * @param IJobList $jobList */ - public function __construct(IDBConnection $connection, IJobList $jobList) { - $this->connection = $connection; - $this->jobList = $jobList; + public function __construct( + private IDBConnection $connection, + private IJobList $jobList, + ) { } /** diff --git a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php index a8138d876e9..ef8e9002e9d 100644 --- a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php +++ b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -31,20 +14,14 @@ use OCP\Migration\IRepairStep; class RegenerateBirthdayCalendars implements IRepairStep { - /** @var IJobList */ - private $jobList; - - /** @var IConfig */ - private $config; - /** * @param IJobList $jobList * @param IConfig $config */ - public function __construct(IJobList $jobList, - IConfig $config) { - $this->jobList = $jobList; - $this->config = $config; + public function __construct( + private IJobList $jobList, + private IConfig $config, + ) { } /** diff --git a/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php index 0b5062fcf3e..7f74390f883 100644 --- a/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php +++ b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -42,15 +22,6 @@ use OCP\Migration\IRepairStep; */ class RegisterBuildReminderIndexBackgroundJob implements IRepairStep { - /** @var IDBConnection */ - private $db; - - /** @var IJobList */ - private $jobList; - - /** @var IConfig */ - private $config; - /** @var string */ private const CONFIG_KEY = 'buildCalendarReminderIndex'; @@ -59,12 +30,11 @@ class RegisterBuildReminderIndexBackgroundJob implements IRepairStep { * @param IJobList $jobList * @param IConfig $config */ - public function __construct(IDBConnection $db, - IJobList $jobList, - IConfig $config) { - $this->db = $db; - $this->jobList = $jobList; - $this->config = $config; + public function __construct( + private IDBConnection $db, + private IJobList $jobList, + private IConfig $config, + ) { } /** @@ -87,8 +57,8 @@ class RegisterBuildReminderIndexBackgroundJob implements IRepairStep { $query = $this->db->getQueryBuilder(); $query->select($query->createFunction('MAX(' . $query->getColumnName('id') . ')')) ->from('calendarobjects'); - $result = $query->execute(); - $maxId = (int) $result->fetchOne(); + $result = $query->executeQuery(); + $maxId = (int)$result->fetchOne(); $result->closeCursor(); $output->info('Add background job'); diff --git a/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php b/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php new file mode 100644 index 00000000000..9d77aefafd2 --- /dev/null +++ b/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; +use OCP\BackgroundJob\IJobList; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class RegisterUpdateCalendarResourcesRoomBackgroundJob implements IRepairStep { + public function __construct( + private readonly IJobList $jobList, + ) { + } + + public function getName() { + return 'Register a background job to update rooms and resources'; + } + + public function run(IOutput $output) { + $this->jobList->add(UpdateCalendarResourcesRoomsBackgroundJob::class); + } +} diff --git a/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php b/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php index 36108ddadfa..f0d208f4f33 100644 --- a/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php +++ b/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -33,11 +15,9 @@ use OCP\Migration\IRepairStep; class RemoveClassifiedEventActivity implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } /** @@ -76,7 +56,7 @@ class RemoveClassifiedEventActivity implements IRepairStep { ->from('calendarobjects', 'o') ->leftJoin('o', 'calendars', 'c', $query->expr()->eq('c.id', 'o.calendarid')) ->where($query->expr()->eq('o.classification', $query->createNamedParameter(CalDavBackend::CLASSIFICATION_PRIVATE))); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { if ($row['principaluri'] === null) { @@ -87,7 +67,7 @@ class RemoveClassifiedEventActivity implements IRepairStep { ->setParameter('type', 'calendar') ->setParameter('calendar_id', $row['calendarid']) ->setParameter('event_uid', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '"') . '%'); - $deletedEvents += $delete->execute(); + $deletedEvents += $delete->executeStatement(); } $result->closeCursor(); @@ -110,7 +90,7 @@ class RemoveClassifiedEventActivity implements IRepairStep { ->from('calendarobjects', 'o') ->leftJoin('o', 'calendars', 'c', $query->expr()->eq('c.id', 'o.calendarid')) ->where($query->expr()->eq('o.classification', $query->createNamedParameter(CalDavBackend::CLASSIFICATION_CONFIDENTIAL))); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { if ($row['principaluri'] === null) { @@ -122,7 +102,7 @@ class RemoveClassifiedEventActivity implements IRepairStep { ->setParameter('calendar_id', $row['calendarid']) ->setParameter('event_uid', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '"') . '%') ->setParameter('filtered_name', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '","name":"Busy"') . '%'); - $deletedEvents += $delete->execute(); + $deletedEvents += $delete->executeStatement(); } $result->closeCursor(); diff --git a/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php b/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php index 38d395b2c81..e2b2b701e74 100644 --- a/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php +++ b/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -32,12 +15,6 @@ use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** @var IUserManager */ - private $userManager; - /** @var int */ private $progress = 0; @@ -46,9 +23,10 @@ class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep { private const SUBSCRIPTIONS_CHUNK_SIZE = 1000; - public function __construct(IDBConnection $connection, IUserManager $userManager) { - $this->connection = $connection; - $this->userManager = $userManager; + public function __construct( + private IDBConnection $connection, + private IUserManager $userManager, + ) { } /** @@ -113,7 +91,7 @@ class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep { while ($row = $result->fetch()) { $username = $this->getPrincipal($row['principaluri']); if (!$this->userManager->userExists($username)) { - $this->orphanSubscriptionIds[] = (int) $row['id']; + $this->orphanSubscriptionIds[] = (int)$row['id']; } } $result->closeCursor(); diff --git a/apps/dav/lib/Migration/RemoveObjectProperties.php b/apps/dav/lib/Migration/RemoveObjectProperties.php index b771b70e684..f09293ae0bb 100644 --- a/apps/dav/lib/Migration/RemoveObjectProperties.php +++ b/apps/dav/lib/Migration/RemoveObjectProperties.php @@ -1,23 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, Thomas Citharel <nextcloud@tcit.fr>. - * - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @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: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Migration; @@ -31,16 +16,14 @@ class RemoveObjectProperties implements IRepairStep { private const ME_CARD_PROPERTY = '{http://calendarserver.org/ns/}me-card'; private const CALENDAR_TRANSP_PROPERTY = '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp'; - /** @var IDBConnection */ - private $connection; - /** * RemoveObjectProperties constructor. * * @param IDBConnection $connection */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } /** diff --git a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php index a36d43d29df..143dc3cd1e6 100644 --- a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php +++ b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php @@ -3,108 +3,50 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; -use OCA\DAV\CalDAV\CalDavBackend; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; +use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob; +use OCP\BackgroundJob\IJobList; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; class RemoveOrphanEventsAndContacts implements IRepairStep { - - /** @var IDBConnection */ - private $connection; - - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private readonly IJobList $jobList, + ) { } - /** - * @inheritdoc - */ public function getName(): string { - return 'Clean up orphan event and contact data'; + return 'Queue jobs to clean up orphan event and contact data'; } - /** - * @inheritdoc - */ - public function run(IOutput $output) { - $orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendars', 'calendarid'); - $output->info(sprintf('%d events without a calendar have been cleaned up', $orphanItems)); - $orphanItems = $this->removeOrphanChildren('calendarobjects_props', 'calendarobjects', 'objectid'); - $output->info(sprintf('%d properties without an events have been cleaned up', $orphanItems)); - $orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendars', 'calendarid'); - $output->info(sprintf('%d changes without a calendar have been cleaned up', $orphanItems)); + public function run(IOutput $output): void { + $this->queueJob('calendarobjects', 'calendars', 'calendarid', '%d events without a calendar have been cleaned up'); + $this->queueJob('calendarobjects_props', 'calendarobjects', 'objectid', '%d properties without an events have been cleaned up'); + $this->queueJob('calendarchanges', 'calendars', 'calendarid', '%d changes without a calendar have been cleaned up'); - $orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendarsubscriptions', 'calendarid'); - $output->info(sprintf('%d cached events without a calendar subscription have been cleaned up', $orphanItems)); - $orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendarsubscriptions', 'calendarid'); - $output->info(sprintf('%d changes without a calendar subscription have been cleaned up', $orphanItems)); + $this->queueJob('calendarobjects', 'calendarsubscriptions', 'calendarid', '%d cached events without a calendar subscription have been cleaned up'); + $this->queueJob('calendarchanges', 'calendarsubscriptions', 'calendarid', '%d changes without a calendar subscription have been cleaned up'); - $orphanItems = $this->removeOrphanChildren('cards', 'addressbooks', 'addressbookid'); - $output->info(sprintf('%d contacts without an addressbook have been cleaned up', $orphanItems)); - $orphanItems = $this->removeOrphanChildren('cards_properties', 'cards', 'cardid'); - $output->info(sprintf('%d properties without a contact have been cleaned up', $orphanItems)); - $orphanItems = $this->removeOrphanChildren('addressbookchanges', 'addressbooks', 'addressbookid'); - $output->info(sprintf('%d changes without an addressbook have been cleaned up', $orphanItems)); + $this->queueJob('cards', 'addressbooks', 'addressbookid', '%d contacts without an addressbook have been cleaned up'); + $this->queueJob('cards_properties', 'cards', 'cardid', '%d properties without a contact have been cleaned up'); + $this->queueJob('addressbookchanges', 'addressbooks', 'addressbookid', '%d changes without an addressbook have been cleaned up'); } - protected function removeOrphanChildren($childTable, $parentTable, $parentId): int { - $qb = $this->connection->getQueryBuilder(); - - $qb->select('c.id') - ->from($childTable, 'c') - ->leftJoin('c', $parentTable, 'p', $qb->expr()->eq('c.' . $parentId, 'p.id')) - ->where($qb->expr()->isNull('p.id')); - - if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) { - $calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR; - $qb->andWhere($qb->expr()->eq('c.calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); - } - - $result = $qb->execute(); - - $orphanItems = []; - while ($row = $result->fetch()) { - $orphanItems[] = (int) $row['id']; - } - $result->closeCursor(); - - if (!empty($orphanItems)) { - $qb->delete($childTable) - ->where($qb->expr()->in('id', $qb->createParameter('ids'))); - - $orphanItemsBatch = array_chunk($orphanItems, 200); - foreach ($orphanItemsBatch as $items) { - $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY); - $qb->execute(); - } - } - - return count($orphanItems); + private function queueJob( + string $childTable, + string $parentTable, + string $parentId, + string $logMessage, + ): void { + $this->jobList->add(CleanupOrphanedChildrenJob::class, [ + CleanupOrphanedChildrenJob::ARGUMENT_CHILD_TABLE => $childTable, + CleanupOrphanedChildrenJob::ARGUMENT_PARENT_TABLE => $parentTable, + CleanupOrphanedChildrenJob::ARGUMENT_PARENT_ID => $parentId, + CleanupOrphanedChildrenJob::ARGUMENT_LOG_MESSAGE => $logMessage, + ]); } } diff --git a/apps/dav/lib/Migration/Version1004Date20170825134824.php b/apps/dav/lib/Migration/Version1004Date20170825134824.php index a7cbaa78ef2..4bf9637b697 100644 --- a/apps/dav/lib/Migration/Version1004Date20170825134824.php +++ b/apps/dav/lib/Migration/Version1004Date20170825134824.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -383,6 +364,7 @@ class Version1004Date20170825134824 extends SimpleMigrationStep { ]); $table->setPrimaryKey(['id']); $table->addIndex(['principaluri'], 'schedulobj_principuri_index'); + $table->addIndex(['lastmodified'], 'schedulobj_lastmodified_idx'); } if (!$schema->hasTable('cards_properties')) { @@ -490,6 +472,9 @@ class Version1004Date20170825134824 extends SimpleMigrationStep { ]); $table->setPrimaryKey(['id']); $table->addUniqueIndex(['principaluri', 'resourceid', 'type', 'publicuri'], 'dav_shares_index'); + // modified on 2024-6-21 to add performance improving indices on new instances + $table->addIndex(['resourceid', 'type'], 'dav_shares_resourceid_type'); + $table->addIndex(['resourceid', 'access'], 'dav_shares_resourceid_access'); } return $schema; } diff --git a/apps/dav/lib/Migration/Version1004Date20170919104507.php b/apps/dav/lib/Migration/Version1004Date20170919104507.php index 54548f30aca..678d92d2b83 100644 --- a/apps/dav/lib/Migration/Version1004Date20170919104507.php +++ b/apps/dav/lib/Migration/Version1004Date20170919104507.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1004Date20170924124212.php b/apps/dav/lib/Migration/Version1004Date20170924124212.php index 4cc795e1aec..4d221e91132 100644 --- a/apps/dav/lib/Migration/Version1004Date20170924124212.php +++ b/apps/dav/lib/Migration/Version1004Date20170924124212.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -46,7 +29,10 @@ class Version1004Date20170924124212 extends SimpleMigrationStep { $table->addIndex(['addressbookid', 'uri'], 'cards_abiduri'); $table = $schema->getTable('cards_properties'); - $table->addIndex(['addressbookid'], 'cards_prop_abid'); + // Removed later on + // $table->addIndex(['addressbookid'], 'cards_prop_abid'); + // Added later on + $table->addIndex(['addressbookid', 'name', 'value'], 'cards_prop_abid_name_value', ); return $schema; } diff --git a/apps/dav/lib/Migration/Version1004Date20170926103422.php b/apps/dav/lib/Migration/Version1004Date20170926103422.php index 6d88effc240..ec56e035006 100644 --- a/apps/dav/lib/Migration/Version1004Date20170926103422.php +++ b/apps/dav/lib/Migration/Version1004Date20170926103422.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -32,7 +15,7 @@ class Version1004Date20170926103422 extends BigIntMigration { /** * @return array Returns an array with the following structure - * ['table1' => ['column1', 'column2'], ...] + * ['table1' => ['column1', 'column2'], ...] * @since 13.0.0 */ protected function getColumnsByTable() { diff --git a/apps/dav/lib/Migration/Version1005Date20180413093149.php b/apps/dav/lib/Migration/Version1005Date20180413093149.php index 6609b2396dd..db071c65d98 100644 --- a/apps/dav/lib/Migration/Version1005Date20180413093149.php +++ b/apps/dav/lib/Migration/Version1005Date20180413093149.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1005Date20180530124431.php b/apps/dav/lib/Migration/Version1005Date20180530124431.php index 9b0868f9374..b5f9ff26962 100644 --- a/apps/dav/lib/Migration/Version1005Date20180530124431.php +++ b/apps/dav/lib/Migration/Version1005Date20180530124431.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1006Date20180619154313.php b/apps/dav/lib/Migration/Version1006Date20180619154313.php index 36e35f615ac..231861a68c4 100644 --- a/apps/dav/lib/Migration/Version1006Date20180619154313.php +++ b/apps/dav/lib/Migration/Version1006Date20180619154313.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1006Date20180628111625.php b/apps/dav/lib/Migration/Version1006Date20180628111625.php index 8c3a8e4938c..f4be26e6ad0 100644 --- a/apps/dav/lib/Migration/Version1006Date20180628111625.php +++ b/apps/dav/lib/Migration/Version1006Date20180628111625.php @@ -3,29 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -70,6 +49,7 @@ class Version1006Date20180628111625 extends SimpleMigrationStep { $calendarObjectsTable->dropIndex('calobjects_index'); } $calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uri'], 'calobjects_index'); + $calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uid'], 'calobjects_by_uid_index'); } if ($schema->hasTable('calendarobjects_props')) { diff --git a/apps/dav/lib/Migration/Version1008Date20181030113700.php b/apps/dav/lib/Migration/Version1008Date20181030113700.php index e9751159913..d2354a185df 100644 --- a/apps/dav/lib/Migration/Version1008Date20181030113700.php +++ b/apps/dav/lib/Migration/Version1008Date20181030113700.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv@protonmail.com) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1008Date20181105104826.php b/apps/dav/lib/Migration/Version1008Date20181105104826.php index 7b0c9861a98..82612307cbb 100644 --- a/apps/dav/lib/Migration/Version1008Date20181105104826.php +++ b/apps/dav/lib/Migration/Version1008Date20181105104826.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -37,16 +17,14 @@ use OCP\Migration\SimpleMigrationStep; class Version1008Date20181105104826 extends SimpleMigrationStep { - /** @var IDBConnection */ - private $connection; - /** * Version1008Date20181105104826 constructor. * * @param IDBConnection $connection */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } /** diff --git a/apps/dav/lib/Migration/Version1008Date20181105104833.php b/apps/dav/lib/Migration/Version1008Date20181105104833.php index 4b4e9cc535e..3d4094d8072 100644 --- a/apps/dav/lib/Migration/Version1008Date20181105104833.php +++ b/apps/dav/lib/Migration/Version1008Date20181105104833.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1008Date20181105110300.php b/apps/dav/lib/Migration/Version1008Date20181105110300.php index 9a69d106412..72e8dee1bf8 100644 --- a/apps/dav/lib/Migration/Version1008Date20181105110300.php +++ b/apps/dav/lib/Migration/Version1008Date20181105110300.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -37,16 +17,14 @@ use OCP\Migration\SimpleMigrationStep; class Version1008Date20181105110300 extends SimpleMigrationStep { - /** @var IDBConnection */ - private $connection; - /** * Version1008Date20181105110300 constructor. * * @param IDBConnection $connection */ - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } /** diff --git a/apps/dav/lib/Migration/Version1008Date20181105112049.php b/apps/dav/lib/Migration/Version1008Date20181105112049.php index 8dd7d695e68..eb18eacb0b1 100644 --- a/apps/dav/lib/Migration/Version1008Date20181105112049.php +++ b/apps/dav/lib/Migration/Version1008Date20181105112049.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1008Date20181114084440.php b/apps/dav/lib/Migration/Version1008Date20181114084440.php index 91f4211e3fa..3718f16f54a 100644 --- a/apps/dav/lib/Migration/Version1008Date20181114084440.php +++ b/apps/dav/lib/Migration/Version1008Date20181114084440.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1011Date20190725113607.php b/apps/dav/lib/Migration/Version1011Date20190725113607.php index a6673378259..4524dca9c83 100644 --- a/apps/dav/lib/Migration/Version1011Date20190725113607.php +++ b/apps/dav/lib/Migration/Version1011Date20190725113607.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1011Date20190806104428.php b/apps/dav/lib/Migration/Version1011Date20190806104428.php index 7c41bce87f5..183dcd4bf1e 100644 --- a/apps/dav/lib/Migration/Version1011Date20190806104428.php +++ b/apps/dav/lib/Migration/Version1011Date20190806104428.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1012Date20190808122342.php b/apps/dav/lib/Migration/Version1012Date20190808122342.php index e72b552cac1..717bcbc1119 100644 --- a/apps/dav/lib/Migration/Version1012Date20190808122342.php +++ b/apps/dav/lib/Migration/Version1012Date20190808122342.php @@ -3,29 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1016Date20201109085907.php b/apps/dav/lib/Migration/Version1016Date20201109085907.php index 6828c71301a..f0f4b8b77c1 100644 --- a/apps/dav/lib/Migration/Version1016Date20201109085907.php +++ b/apps/dav/lib/Migration/Version1016Date20201109085907.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1017Date20210216083742.php b/apps/dav/lib/Migration/Version1017Date20210216083742.php index c5347d773d1..43bece200b8 100644 --- a/apps/dav/lib/Migration/Version1017Date20210216083742.php +++ b/apps/dav/lib/Migration/Version1017Date20210216083742.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1018Date20210312100735.php b/apps/dav/lib/Migration/Version1018Date20210312100735.php index b0ea5146caa..8199d3e9cd2 100644 --- a/apps/dav/lib/Migration/Version1018Date20210312100735.php +++ b/apps/dav/lib/Migration/Version1018Date20210312100735.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1024Date20211221144219.php b/apps/dav/lib/Migration/Version1024Date20211221144219.php index b93f8ac801e..656a50809cd 100644 --- a/apps/dav/lib/Migration/Version1024Date20211221144219.php +++ b/apps/dav/lib/Migration/Version1024Date20211221144219.php @@ -1,7 +1,10 @@ <?php declare(strict_types=1); - +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ namespace OCA\DAV\Migration; use Closure; diff --git a/apps/dav/lib/Migration/Version1025Date20240308063933.php b/apps/dav/lib/Migration/Version1025Date20240308063933.php index a75fc85eccc..d84acf8fea9 100644 --- a/apps/dav/lib/Migration/Version1025Date20240308063933.php +++ b/apps/dav/lib/Migration/Version1025Date20240308063933.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2024 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; use Closure; +use OCP\AppFramework\Services\IAppConfig; use OCP\DB\ISchemaWrapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Types; @@ -36,10 +20,10 @@ use OCP\Migration\SimpleMigrationStep; class Version1025Date20240308063933 extends SimpleMigrationStep { - private IDBConnection $db; - - public function __construct(IDBConnection $db) { - $this->db = $db; + public function __construct( + private IAppConfig $appConfig, + private IDBConnection $db, + ) { } /** @@ -67,7 +51,22 @@ class Version1025Date20240308063933 extends SimpleMigrationStep { } public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void { + // The threshold is higher than the default of \OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob + // but small enough to fit into a cluster transaction size. + // For a 50k users instance that would still keep 10 changes on average. + $limit = max(1, (int)$this->appConfig->getAppValue('totalNumberOfSyncTokensToKeep', '500000')); + foreach (['addressbookchanges', 'calendarchanges'] as $tableName) { + $thresholdSelect = $this->db->getQueryBuilder(); + $thresholdSelect->select('id') + ->from($tableName) + ->orderBy('id', 'desc') + ->setFirstResult($limit) + ->setMaxResults(1); + $oldestIdResult = $thresholdSelect->executeQuery(); + $oldestId = $oldestIdResult->fetchColumn(); + $oldestIdResult->closeCursor(); + $qb = $this->db->getQueryBuilder(); $update = $qb->update($tableName) @@ -76,7 +75,15 @@ class Version1025Date20240308063933 extends SimpleMigrationStep { $qb->expr()->eq('created_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)), ); + // If there is a lot of data we only set timestamp for the most recent rows + // because the rest will be deleted by \OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob + // anyway. + if ($oldestId !== false) { + $update->andWhere($qb->expr()->gt('id', $qb->createNamedParameter($oldestId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + } + $updated = $update->executeStatement(); + $output->debug('Added a default creation timestamp to ' . $updated . ' rows in ' . $tableName); } } diff --git a/apps/dav/lib/Migration/Version1027Date20230504122946.php b/apps/dav/lib/Migration/Version1027Date20230504122946.php index 28361011436..89c192a8419 100644 --- a/apps/dav/lib/Migration/Version1027Date20230504122946.php +++ b/apps/dav/lib/Migration/Version1027Date20230504122946.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -37,10 +20,12 @@ use Psr\Log\LoggerInterface; use Throwable; class Version1027Date20230504122946 extends SimpleMigrationStep { - public function __construct(private SyncService $syncService, + public function __construct( + private SyncService $syncService, private LoggerInterface $logger, private IUserManager $userManager, - private IConfig $config) { + private IConfig $config, + ) { } /** * @param IOutput $output @@ -48,7 +33,7 @@ class Version1027Date20230504122946 extends SimpleMigrationStep { * @param array $options */ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { - if($this->userManager->countSeenUsers() > 100 || array_sum($this->userManager->countUsers()) > 100) { + if ($this->userManager->countSeenUsers() > 100 || $this->userManager->countUsersTotal(100) >= 100) { $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'yes'); $output->info('Could not sync system address books during update - too many user records have been found. Please call occ dav:sync-system-addressbook manually.'); return; diff --git a/apps/dav/lib/Migration/Version1029Date20221114151721.php b/apps/dav/lib/Migration/Version1029Date20221114151721.php index 112b240c8ba..dba5e0b1a48 100644 --- a/apps/dav/lib/Migration/Version1029Date20221114151721.php +++ b/apps/dav/lib/Migration/Version1029Date20221114151721.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Murena SAS <akhil.potukuchi.ext@murena.com> - * - * @author Murena SAS <akhil.potukuchi.ext@murena.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; @@ -45,7 +28,7 @@ class Version1029Date20221114151721 extends SimpleMigrationStep { /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); $calendarObjectsTable = $schema->getTable('calendarobjects'); - if(!$calendarObjectsTable->hasIndex('calobj_clssfction_index')) { + if (!$calendarObjectsTable->hasIndex('calobj_clssfction_index')) { $calendarObjectsTable->addIndex(['classification'], 'calobj_clssfction_index'); return $schema; } diff --git a/apps/dav/lib/Migration/Version1029Date20231004091403.php b/apps/dav/lib/Migration/Version1029Date20231004091403.php index 6eb7be8a5cd..1dcbf9c5dfc 100644 --- a/apps/dav/lib/Migration/Version1029Date20231004091403.php +++ b/apps/dav/lib/Migration/Version1029Date20231004091403.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1030Date20240205103243.php b/apps/dav/lib/Migration/Version1030Date20240205103243.php index eb471ccb0a6..72cbcafc444 100644 --- a/apps/dav/lib/Migration/Version1030Date20240205103243.php +++ b/apps/dav/lib/Migration/Version1030Date20240205103243.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2024 Johannes Merkel <mail@johannesgge.de> - * - * @author Johannes Merkel <mail@johannesgge.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Migration; diff --git a/apps/dav/lib/Migration/Version1031Date20240610134258.php b/apps/dav/lib/Migration/Version1031Date20240610134258.php new file mode 100644 index 00000000000..c1242ceb7db --- /dev/null +++ b/apps/dav/lib/Migration/Version1031Date20240610134258.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\Attributes\AddColumn; +use OCP\Migration\Attributes\ColumnType; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +#[AddColumn(table: 'dav_absence', name: 'replacement_user_id', type: ColumnType::STRING)] +#[AddColumn(table: 'dav_absence', name: 'replacement_user_display_name', type: ColumnType::STRING)] +class Version1031Date20240610134258 extends SimpleMigrationStep { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $tableDavAbsence = $schema->getTable('dav_absence'); + + if (!$tableDavAbsence->hasColumn('replacement_user_id')) { + $tableDavAbsence->addColumn('replacement_user_id', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 64, + ]); + } + + if (!$tableDavAbsence->hasColumn('replacement_user_display_name')) { + $tableDavAbsence->addColumn('replacement_user_display_name', Types::STRING, [ + 'notnull' => false, + 'default' => '', + 'length' => 64, + ]); + } + + return $schema; + } + +} diff --git a/apps/dav/lib/Model/ExampleEvent.php b/apps/dav/lib/Model/ExampleEvent.php new file mode 100644 index 00000000000..d2a5b8ad2d1 --- /dev/null +++ b/apps/dav/lib/Model/ExampleEvent.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Model; + +use Sabre\VObject\Component\VCalendar; + +/** + * Simple DTO to store a parsed example event and its UID. + */ +final class ExampleEvent { + public function __construct( + private readonly VCalendar $vCalendar, + private readonly string $uid, + ) { + } + + public function getUid(): string { + return $this->uid; + } + + public function getIcs(): string { + return $this->vCalendar->serialize(); + } +} diff --git a/apps/dav/lib/Paginate/LimitedCopyIterator.php b/apps/dav/lib/Paginate/LimitedCopyIterator.php new file mode 100644 index 00000000000..7f19885bc7d --- /dev/null +++ b/apps/dav/lib/Paginate/LimitedCopyIterator.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Paginate; + +/** + * Save a copy of the first X items into a separate iterator + * + * This allows us to pass the iterator to the cache while keeping a copy + * of the required items. + * + * @extends \AppendIterator<int, int, \Iterator<int, int>> + */ +class LimitedCopyIterator extends \AppendIterator { + private array $skipped = []; + private array $copy = []; + + public function __construct(\Traversable $iterator, int $count, int $offset = 0) { + parent::__construct(); + + if (!$iterator instanceof \Iterator) { + $iterator = new \IteratorIterator($iterator); + } + $iterator = new \NoRewindIterator($iterator); + + $i = 0; + while ($iterator->valid() && ++$i <= $offset) { + $this->skipped[] = $iterator->current(); + $iterator->next(); + } + + while ($iterator->valid() && count($this->copy) < $count) { + $this->copy[] = $iterator->current(); + $iterator->next(); + } + + $this->append(new \ArrayIterator($this->skipped)); + $this->append($this->getRequestedItems()); + $this->append($iterator); + } + + public function getRequestedItems(): \Iterator { + return new \ArrayIterator($this->copy); + } +} diff --git a/apps/dav/lib/Paginate/PaginateCache.php b/apps/dav/lib/Paginate/PaginateCache.php new file mode 100644 index 00000000000..58219b03621 --- /dev/null +++ b/apps/dav/lib/Paginate/PaginateCache.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Paginate; + +use Generator; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IDBConnection; +use OCP\Security\ISecureRandom; + +class PaginateCache { + public const TTL = 60 * 60; + private const CACHE_COUNT_SUFFIX = 'count'; + + private ICache $cache; + + public function __construct( + private IDBConnection $database, + private ISecureRandom $random, + ICacheFactory $cacheFactory, + ) { + $this->cache = $cacheFactory->createDistributed('pagination_'); + } + + /** + * @param string $uri + * @param \Iterator $items + * @return array{'token': string, 'count': int} + */ + public function store(string $uri, \Iterator $items): array { + $token = $this->random->generate(32); + $cacheKey = $this->buildCacheKey($uri, $token); + + $count = 0; + foreach ($items as $item) { + // Add small margin to avoid fetching valid count and then expired entries + $this->cache->set($cacheKey . $count, $item, self::TTL + 60); + ++$count; + } + $this->cache->set($cacheKey . self::CACHE_COUNT_SUFFIX, $count, self::TTL); + + return ['token' => $token, 'count' => $count]; + } + + /** + * @return Generator<mixed> + */ + public function get(string $uri, string $token, int $offset, int $count): Generator { + $cacheKey = $this->buildCacheKey($uri, $token); + $nbItems = $this->cache->get($cacheKey . self::CACHE_COUNT_SUFFIX); + if (!$nbItems || $offset > $nbItems) { + return []; + } + + $lastItem = min($nbItems, $offset + $count); + for ($i = $offset; $i < $lastItem; ++$i) { + yield $this->cache->get($cacheKey . $i); + } + } + + public function exists(string $uri, string $token): bool { + return $this->cache->get($this->buildCacheKey($uri, $token) . self::CACHE_COUNT_SUFFIX) > 0; + } + + private function buildCacheKey(string $uri, string $token): string { + return $token . '_' . crc32($uri) . '_'; + } +} diff --git a/apps/dav/lib/Paginate/PaginatePlugin.php b/apps/dav/lib/Paginate/PaginatePlugin.php new file mode 100644 index 00000000000..c5da18f5c47 --- /dev/null +++ b/apps/dav/lib/Paginate/PaginatePlugin.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Paginate; + +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class PaginatePlugin extends ServerPlugin { + public const PAGINATE_HEADER = 'X-NC-Paginate'; + public const PAGINATE_TOTAL_HEADER = 'X-NC-Paginate-Total'; + public const PAGINATE_TOKEN_HEADER = 'X-NC-Paginate-Token'; + public const PAGINATE_OFFSET_HEADER = 'X-NC-Paginate-Offset'; + public const PAGINATE_COUNT_HEADER = 'X-NC-Paginate-Count'; + + /** @var Server */ + private $server; + + public function __construct( + private PaginateCache $cache, + private int $pageSize = 100, + ) { + } + + public function initialize(Server $server): void { + $this->server = $server; + $server->on('beforeMultiStatus', [$this, 'onMultiStatus']); + $server->on('method:SEARCH', [$this, 'onMethod'], 1); + $server->on('method:PROPFIND', [$this, 'onMethod'], 1); + $server->on('method:REPORT', [$this, 'onMethod'], 1); + } + + public function getFeatures(): array { + return ['nc-paginate']; + } + + public function onMultiStatus(&$fileProperties): void { + $request = $this->server->httpRequest; + if (is_array($fileProperties)) { + $fileProperties = new \ArrayIterator($fileProperties); + } + $url = $request->getUrl(); + if ( + $request->hasHeader(self::PAGINATE_HEADER) + && (!$request->hasHeader(self::PAGINATE_TOKEN_HEADER) || !$this->cache->exists($url, $request->getHeader(self::PAGINATE_TOKEN_HEADER))) + ) { + $pageSize = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize; + $offset = (int)$request->getHeader(self::PAGINATE_OFFSET_HEADER); + $copyIterator = new LimitedCopyIterator($fileProperties, $pageSize, $offset); + ['token' => $token, 'count' => $count] = $this->cache->store($url, $copyIterator); + + $fileProperties = $copyIterator->getRequestedItems(); + $this->server->httpResponse->addHeader(self::PAGINATE_HEADER, 'true'); + $this->server->httpResponse->addHeader(self::PAGINATE_TOKEN_HEADER, $token); + $this->server->httpResponse->addHeader(self::PAGINATE_TOTAL_HEADER, (string)$count); + $request->setHeader(self::PAGINATE_TOKEN_HEADER, $token); + } + } + + public function onMethod(RequestInterface $request, ResponseInterface $response) { + $url = $this->server->httpRequest->getUrl(); + if ( + $request->hasHeader(self::PAGINATE_TOKEN_HEADER) + && $request->hasHeader(self::PAGINATE_OFFSET_HEADER) + && $this->cache->exists($url, $request->getHeader(self::PAGINATE_TOKEN_HEADER)) + ) { + $token = $request->getHeader(self::PAGINATE_TOKEN_HEADER); + $offset = (int)$request->getHeader(self::PAGINATE_OFFSET_HEADER); + $count = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize; + + $items = $this->cache->get($url, $token, $offset, $count); + + $response->setStatus(207); + $response->addHeader(self::PAGINATE_HEADER, 'true'); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = $prefer['return'] === 'minimal'; + + $data = $this->server->generateMultiStatus($items, $minimal); + $response->setBody($data); + + return false; + } + } +} diff --git a/apps/dav/lib/Profiler/ProfilerPlugin.php b/apps/dav/lib/Profiler/ProfilerPlugin.php index cc65c2b75e1..455760fc2bf 100644 --- a/apps/dav/lib/Profiler/ProfilerPlugin.php +++ b/apps/dav/lib/Profiler/ProfilerPlugin.php @@ -2,25 +2,8 @@ declare(strict_types = 1); /** - * @copyright 2021 Carl Schwan <carl@carlschwan.eu> - * - * @author Carl Schwan <carl@carlschwan.eu> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Profiler; @@ -31,10 +14,9 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class ProfilerPlugin extends \Sabre\DAV\ServerPlugin { - private IRequest $request; - - public function __construct(IRequest $request) { - $this->request = $request; + public function __construct( + private IRequest $request, + ) { } /** @return void */ diff --git a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php index 614ddabb7ef..bb098a0f107 100644 --- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php +++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Provisioning\Apple; @@ -32,13 +15,12 @@ use Sabre\DAV\PropPatch; class AppleProvisioningNode implements INode, IProperties { public const FILENAME = 'apple-provisioning.mobileconfig'; - protected $timeFactory; - /** * @param ITimeFactory $timeFactory */ - public function __construct(ITimeFactory $timeFactory) { - $this->timeFactory = $timeFactory; + public function __construct( + protected ITimeFactory $timeFactory, + ) { } /** @@ -76,7 +58,7 @@ class AppleProvisioningNode implements INode, IProperties { return [ '{DAV:}getcontentlength' => 42, - '{DAV:}getlastmodified' => $datetime->format(\DateTimeInterface::RFC2822), + '{DAV:}getlastmodified' => $datetime->format(\DateTimeInterface::RFC7231), ]; } diff --git a/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php b/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php index 98be4e41917..258138caa42 100644 --- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php +++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php @@ -1,31 +1,12 @@ <?php + /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Nils Wittenbrink <nilswittenbrink@web.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Provisioning\Apple; +use OCP\AppFramework\Http; use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; @@ -42,52 +23,22 @@ class AppleProvisioningPlugin extends ServerPlugin { protected $server; /** - * @var IURLGenerator - */ - protected $urlGenerator; - - /** - * @var IUserSession - */ - protected $userSession; - - /** * @var \OC_Defaults */ protected $themingDefaults; /** - * @var IRequest - */ - protected $request; - - /** - * @var IL10N - */ - protected $l10n; - - /** - * @var \Closure - */ - protected $uuidClosure; - - /** * AppleProvisioningPlugin constructor. */ public function __construct( - IUserSession $userSession, - IURLGenerator $urlGenerator, + protected IUserSession $userSession, + protected IURLGenerator $urlGenerator, \OC_Defaults $themingDefaults, - IRequest $request, - IL10N $l10n, - \Closure $uuidClosure + protected IRequest $request, + protected IL10N $l10n, + protected \Closure $uuidClosure, ) { - $this->userSession = $userSession; - $this->urlGenerator = $urlGenerator; $this->themingDefaults = $themingDefaults; - $this->request = $request; - $this->l10n = $l10n; - $this->uuidClosure = $uuidClosure; } /** @@ -117,7 +68,7 @@ class AppleProvisioningPlugin extends ServerPlugin { $useSSL = ($serverProtocol === 'https'); if (!$useSSL) { - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setHeader('Content-Type', 'text/plain; charset=utf-8'); $response->setBody($this->l10n->t('Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.', [$this->themingDefaults->getName()])); @@ -126,11 +77,7 @@ class AppleProvisioningPlugin extends ServerPlugin { $absoluteURL = $this->urlGenerator->getBaseUrl(); $parsedUrl = parse_url($absoluteURL); - if (isset($parsedUrl['port'])) { - $serverPort = $parsedUrl['port']; - } else { - $serverPort = 443; - } + $serverPort = $parsedUrl['port'] ?? 443; $server_url = $parsedUrl['host']; $description = $this->themingDefaults->getName(); @@ -154,7 +101,7 @@ class AppleProvisioningPlugin extends ServerPlugin { $filename = $userId . '-' . AppleProvisioningNode::FILENAME; $xmlSkeleton = $this->getTemplate(); - $body = vsprintf($xmlSkeleton, array_map(function ($v) { + $body = vsprintf($xmlSkeleton, array_map(function (string $v) { return \htmlspecialchars($v, ENT_XML1, 'UTF-8'); }, [ $description, @@ -179,7 +126,7 @@ class AppleProvisioningPlugin extends ServerPlugin { ] )); - $response->setStatus(200); + $response->setStatus(Http::STATUS_OK); $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $response->setBody($body); diff --git a/apps/dav/lib/ResponseDefinitions.php b/apps/dav/lib/ResponseDefinitions.php index e6de3d5a65c..3deafad6704 100644 --- a/apps/dav/lib/ResponseDefinitions.php +++ b/apps/dav/lib/ResponseDefinitions.php @@ -3,33 +3,20 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV; +use OCA\DAV\CalDAV\UpcomingEvent; + /** * @psalm-type DAVOutOfOfficeDataCommon = array{ * userId: string, * message: string, + * replacementUserId: ?string, + * replacementUserDisplayName: ?string, * } * * @psalm-type DAVOutOfOfficeData = DAVOutOfOfficeDataCommon&array{ @@ -46,6 +33,15 @@ namespace OCA\DAV; * endDate: int, * shortMessage: string, * } + * + * @see UpcomingEvent::jsonSerialize + * @psalm-type DAVUpcomingEvent = array{ + * uri: string, + * calendarUri: string, + * start: ?int, + * summary: ?string, + * location: ?string, + * } */ class ResponseDefinitions { } diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index d1f3dbc91bd..870aa0d4540 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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; @@ -44,41 +23,53 @@ use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\GroupPrincipalBackend; use OCA\DAV\DAV\SystemPrincipalBackend; use OCA\DAV\Provisioning\Apple\AppleProvisioningNode; +use OCA\DAV\SystemTag\SystemTagsByIdCollection; +use OCA\DAV\SystemTag\SystemTagsInUseCollection; +use OCA\DAV\SystemTag\SystemTagsRelationsCollection; use OCA\DAV\Upload\CleanupService; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Server; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use Psr\Log\LoggerInterface; use Sabre\DAV\SimpleCollection; class RootCollection extends SimpleCollection { public function __construct() { $l10n = \OC::$server->getL10N('dav'); - $random = \OC::$server->getSecureRandom(); - $logger = \OC::$server->get(LoggerInterface::class); - $userManager = \OC::$server->getUserManager(); - $userSession = \OC::$server->getUserSession(); - $groupManager = \OC::$server->getGroupManager(); - $shareManager = \OC::$server->getShareManager(); - $db = \OC::$server->getDatabaseConnection(); - $dispatcher = \OC::$server->get(IEventDispatcher::class); - $config = \OC::$server->get(IConfig::class); - $proxyMapper = \OC::$server->query(ProxyMapper::class); - $rootFolder = \OCP\Server::get(IRootFolder::class); + $random = Server::get(ISecureRandom::class); + $logger = Server::get(LoggerInterface::class); + $userManager = Server::get(IUserManager::class); + $userSession = Server::get(IUserSession::class); + $groupManager = Server::get(IGroupManager::class); + $shareManager = Server::get(\OCP\Share\IManager::class); + $db = Server::get(IDBConnection::class); + $dispatcher = Server::get(IEventDispatcher::class); + $config = Server::get(IConfig::class); + $proxyMapper = Server::get(ProxyMapper::class); + $rootFolder = Server::get(IRootFolder::class); $userPrincipalBackend = new Principal( $userManager, $groupManager, - \OC::$server->get(IAccountManager::class), + Server::get(IAccountManager::class), $shareManager, - \OC::$server->getUserSession(), - \OC::$server->getAppManager(), + Server::get(IUserSession::class), + Server::get(IAppManager::class), $proxyMapper, - \OC::$server->get(KnownUserService::class), - \OC::$server->getConfig(), + Server::get(KnownUserService::class), + Server::get(IConfig::class), \OC::$server->getL10NFactory() ); @@ -96,10 +87,8 @@ class RootCollection extends SimpleCollection { $systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system'); $systemPrincipals->disableListing = $disableListing; $calendarResourcePrincipals = new Collection($calendarResourcePrincipalBackend, 'principals/calendar-resources'); - $calendarResourcePrincipals->disableListing = $disableListing; $calendarRoomPrincipals = new Collection($calendarRoomPrincipalBackend, 'principals/calendar-rooms'); - $calendarRoomPrincipals->disableListing = $disableListing; - $calendarSharingBackend = \OC::$server->get(Backend::class); + $calendarSharingBackend = Server::get(Backend::class); $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; @@ -124,37 +113,35 @@ class RootCollection extends SimpleCollection { $publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config, $logger); - $systemTagCollection = new SystemTag\SystemTagsByIdCollection( - \OC::$server->getSystemTagManager(), - \OC::$server->getUserSession(), - $groupManager - ); - $systemTagRelationsCollection = new SystemTag\SystemTagsRelationsCollection( - \OC::$server->getSystemTagManager(), - \OC::$server->getSystemTagObjectMapper(), - \OC::$server->getUserSession(), + $systemTagCollection = Server::get(SystemTagsByIdCollection::class); + $systemTagRelationsCollection = new SystemTagsRelationsCollection( + Server::get(ISystemTagManager::class), + Server::get(ISystemTagObjectMapper::class), + Server::get(IUserSession::class), $groupManager, $dispatcher, $rootFolder, ); - $systemTagInUseCollection = \OCP\Server::get(SystemTag\SystemTagsInUseCollection::class); + $systemTagInUseCollection = Server::get(SystemTagsInUseCollection::class); $commentsCollection = new Comments\RootCollection( - \OC::$server->getCommentsManager(), + Server::get(ICommentsManager::class), $userManager, - \OC::$server->getUserSession(), + Server::get(IUserSession::class), $dispatcher, $logger ); - $contactsSharingBackend = \OC::$server->get(\OCA\DAV\CardDAV\Sharing\Backend::class); + $contactsSharingBackend = Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class); + $config = Server::get(IConfig::class); - $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); + $pluginManager = new PluginManager(\OC::$server, Server::get(IAppManager::class)); $usersCardDavBackend = new CardDavBackend( $db, $userPrincipalBackend, $userManager, $dispatcher, $contactsSharingBackend, + $config ); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; @@ -165,6 +152,7 @@ class RootCollection extends SimpleCollection { $userManager, $dispatcher, $contactsSharingBackend, + $config ); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; @@ -172,14 +160,18 @@ class RootCollection extends SimpleCollection { $uploadCollection = new Upload\RootCollection( $userPrincipalBackend, 'principals/users', - \OC::$server->query(CleanupService::class)); + Server::get(CleanupService::class), + $rootFolder, + $userSession, + $shareManager, + ); $uploadCollection->disableListing = $disableListing; $avatarCollection = new Avatars\RootCollection($userPrincipalBackend, 'principals/users'); $avatarCollection->disableListing = $disableListing; $appleProvisioning = new AppleProvisioningNode( - \OC::$server->query(ITimeFactory::class)); + Server::get(ITimeFactory::class)); $children = [ new SimpleCollection('principals', [ diff --git a/apps/dav/lib/Search/ACalendarSearchProvider.php b/apps/dav/lib/Search/ACalendarSearchProvider.php index 82c15efec64..331d05cb500 100644 --- a/apps/dav/lib/Search/ACalendarSearchProvider.php +++ b/apps/dav/lib/Search/ACalendarSearchProvider.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Search; @@ -40,18 +23,6 @@ use Sabre\VObject\Reader; */ abstract class ACalendarSearchProvider implements IProvider { - /** @var IAppManager */ - protected $appManager; - - /** @var IL10N */ - protected $l10n; - - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var CalDavBackend */ - protected $backend; - /** * ACalendarSearchProvider constructor. * @@ -60,14 +31,12 @@ abstract class ACalendarSearchProvider implements IProvider { * @param IURLGenerator $urlGenerator * @param CalDavBackend $backend */ - public function __construct(IAppManager $appManager, - IL10N $l10n, - IURLGenerator $urlGenerator, - CalDavBackend $backend) { - $this->appManager = $appManager; - $this->l10n = $l10n; - $this->urlGenerator = $urlGenerator; - $this->backend = $backend; + public function __construct( + protected IAppManager $appManager, + protected IL10N $l10n, + protected IURLGenerator $urlGenerator, + protected CalDavBackend $backend, + ) { } /** @@ -81,7 +50,7 @@ abstract class ACalendarSearchProvider implements IProvider { $calendars = $this->backend->getCalendarsForUser($principalUri); $calendarsById = []; foreach ($calendars as $calendar) { - $calendarsById[(int) $calendar['id']] = $calendar; + $calendarsById[(int)$calendar['id']] = $calendar; } return $calendarsById; @@ -98,7 +67,7 @@ abstract class ACalendarSearchProvider implements IProvider { $subscriptions = $this->backend->getSubscriptionsForUser($principalUri); $subscriptionsById = []; foreach ($subscriptions as $subscription) { - $subscriptionsById[(int) $subscription['id']] = $subscription; + $subscriptionsById[(int)$subscription['id']] = $subscription; } return $subscriptionsById; diff --git a/apps/dav/lib/Search/ContactsSearchProvider.php b/apps/dav/lib/Search/ContactsSearchProvider.php index 2a0e70bf045..158c0d0813e 100644 --- a/apps/dav/lib/Search/ContactsSearchProvider.php +++ b/apps/dav/lib/Search/ContactsSearchProvider.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Search; @@ -100,7 +81,7 @@ class ContactsSearchProvider implements IFilteringProvider { $addressBooks = $this->backend->getAddressBooksForUser($principalUri); $addressBooksById = []; foreach ($addressBooks as $addressBook) { - $addressBooksById[(int) $addressBook['id']] = $addressBook; + $addressBooksById[(int)$addressBook['id']] = $addressBook; } $searchResults = $this->backend->searchPrincipalUri( @@ -128,9 +109,14 @@ class ContactsSearchProvider implements IFilteringProvider { $title = (string)$vCard->FN; $subline = $this->generateSubline($vCard); - $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID); + $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string)$vCard->UID); - return new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true); + $result = new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true); + $result->addAttribute('displayName', $title); + $result->addAttribute('email', $subline); + $result->addAttribute('phoneNumber', (string)$vCard->TEL); + + return $result; }, $searchResults); return SearchResult::paginated( diff --git a/apps/dav/lib/Search/EventsSearchProvider.php b/apps/dav/lib/Search/EventsSearchProvider.php index dd87f486a1d..55fba40918a 100644 --- a/apps/dav/lib/Search/EventsSearchProvider.php +++ b/apps/dav/lib/Search/EventsSearchProvider.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Search; @@ -36,6 +17,7 @@ use OCP\Search\SearchResultEntry; use Sabre\VObject\Component; use Sabre\VObject\DateTimeParser; use Sabre\VObject\Property; +use Sabre\VObject\Property\ICalendar\DateTime; use function array_combine; use function array_fill; use function array_key_exists; @@ -176,8 +158,16 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering $calendar = $subscriptionsById[$eventRow['calendarid']]; } $resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']); + $result = new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false); + + $dtStart = $component->DTSTART; - return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false); + if ($dtStart instanceof DateTime) { + $startDateTime = $dtStart->getDateTime()->format('U'); + $result->addAttribute('createdAt', $startDateTime); + } + + return $result; }, $searchResults); return SearchResult::paginated( @@ -204,7 +194,7 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering protected function getDavUrlForCalendarObject( string $principalUri, string $calendarUri, - string $calendarObjectUri + string $calendarObjectUri, ): string { [,, $principalId] = explode('/', $principalUri, 3); diff --git a/apps/dav/lib/Search/TasksSearchProvider.php b/apps/dav/lib/Search/TasksSearchProvider.php index 5cb64556457..15baf070e81 100644 --- a/apps/dav/lib/Search/TasksSearchProvider.php +++ b/apps/dav/lib/Search/TasksSearchProvider.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Search; @@ -141,7 +121,7 @@ class TasksSearchProvider extends ACalendarSearchProvider { ): string { return $this->urlGenerator->getAbsoluteURL( $this->urlGenerator->linkToRoute('tasks.page.index') - . '#/calendars/' + . 'calendars/' . $calendarUri . '/tasks/' . $taskUri diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index deee381d24c..a92e162f1b0 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -1,51 +1,34 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/ - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Brandon Kirsch <brandonkirsch@github.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV; +use OC\Files\Filesystem; use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\BulkUpload\BulkUploadPlugin; +use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin; use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\EventComparisonService; +use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin; +use OCA\DAV\CalDAV\Publishing\PublishPlugin; +use OCA\DAV\CalDAV\Schedule\IMipPlugin; +use OCA\DAV\CalDAV\Schedule\IMipService; use OCA\DAV\CalDAV\Security\RateLimitingPlugin; +use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin; use OCA\DAV\CardDAV\HasPhotoPlugin; use OCA\DAV\CardDAV\ImageExportPlugin; use OCA\DAV\CardDAV\MultiGetExportPlugin; use OCA\DAV\CardDAV\PhotoCache; +use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin; +use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin; use OCA\DAV\Comments\CommentsPlugin; use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; +use OCA\DAV\Connector\Sabre\AppleQuirksPlugin; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\BearerAuth; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; @@ -55,34 +38,63 @@ use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin; use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin; use OCA\DAV\Connector\Sabre\DavAclPlugin; use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\FakeLockerPlugin; use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\DAV\Connector\Sabre\FilesReportPlugin; +use OCA\DAV\Connector\Sabre\LockPlugin; +use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin; +use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin; use OCA\DAV\Connector\Sabre\QuotaPlugin; use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin; use OCA\DAV\Connector\Sabre\SharesPlugin; use OCA\DAV\Connector\Sabre\TagsPlugin; +use OCA\DAV\Connector\Sabre\ZipFolderPlugin; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\PublicAuth; use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Events\SabrePluginAddEvent; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCA\DAV\Files\FileSearchBackend; use OCA\DAV\Files\LazySearchBackend; +use OCA\DAV\Paginate\PaginatePlugin; use OCA\DAV\Profiler\ProfilerPlugin; use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin; use OCA\DAV\SystemTag\SystemTagPlugin; use OCA\DAV\Upload\ChunkingPlugin; use OCA\DAV\Upload\ChunkingV2Plugin; +use OCA\DAV\Upload\UploadAutoMkcolPlugin; +use OCA\Theming\ThemingDefaults; +use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Comments\ICommentsManager; +use OCP\Defaults; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IFilenameValidator; +use OCP\Files\IRootFolder; use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\IAppConfig; use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IPreview; use OCP\IRequest; +use OCP\ISession; +use OCP\ITagManager; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Mail\IMailer; use OCP\Profiler\IProfiler; use OCP\SabrePluginEvent; +use OCP\Security\Bruteforce\IThrottler; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use Psr\Log\LoggerInterface; use Sabre\CardDAV\VCFExportPlugin; use Sabre\DAV\Auth\Plugin; @@ -90,40 +102,40 @@ use Sabre\DAV\UUIDUtil; use SearchDAV\DAV\SearchPlugin; class Server { - private IRequest $request; - private string $baseUri; public Connector\Sabre\Server $server; private IProfiler $profiler; - public function __construct(IRequest $request, string $baseUri) { - $this->profiler = \OC::$server->get(IProfiler::class); + public function __construct( + private IRequest $request, + private string $baseUri, + ) { + $debugEnabled = \OCP\Server::get(IConfig::class)->getSystemValue('debug', false); + $this->profiler = \OCP\Server::get(IProfiler::class); if ($this->profiler->isEnabled()) { /** @var IEventLogger $eventLogger */ - $eventLogger = \OC::$server->get(IEventLogger::class); + $eventLogger = \OCP\Server::get(IEventLogger::class); $eventLogger->start('runtime', 'DAV Runtime'); } - $this->request = $request; - $this->baseUri = $baseUri; - $logger = \OC::$server->get(LoggerInterface::class); - /** @var IEventDispatcher $dispatcher */ - $dispatcher = \OC::$server->get(IEventDispatcher::class); + $logger = \OCP\Server::get(LoggerInterface::class); + $eventDispatcher = \OCP\Server::get(IEventDispatcher::class); $root = new RootCollection(); $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root)); + $this->server->setLogger($logger); // Add maintenance plugin - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav'))); + $this->server->addPlugin(new MaintenancePlugin(\OCP\Server::get(IConfig::class), \OC::$server->getL10N('dav'))); - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\AppleQuirksPlugin()); + $this->server->addPlugin(new AppleQuirksPlugin()); // Backends $authBackend = new Auth( - \OC::$server->getSession(), - \OC::$server->getUserSession(), - \OC::$server->getRequest(), - \OC::$server->getTwoFactorAuthManager(), - \OC::$server->getBruteForceThrottler() + \OCP\Server::get(ISession::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IRequest::class), + \OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class), + \OCP\Server::get(IThrottler::class) ); // Set URL explicitly due to reverse-proxy situations @@ -131,7 +143,10 @@ class Server { $this->server->setBaseUri($this->baseUri); $this->server->addPlugin(new ProfilerPlugin($this->request)); - $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); + $this->server->addPlugin(new BlockLegacyClientPlugin( + \OCP\Server::get(IConfig::class), + \OCP\Server::get(ThemingDefaults::class), + )); $this->server->addPlugin(new AnonymousOptionsPlugin()); $authPlugin = new Plugin(); $authPlugin->addBackend(new PublicAuth()); @@ -139,29 +154,32 @@ class Server { // allow setup of additional auth backends $event = new SabrePluginEvent($this->server); - $dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event); + $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event); $newAuthEvent = new SabrePluginAuthInitEvent($this->server); - $dispatcher->dispatchTyped($newAuthEvent); + $eventDispatcher->dispatchTyped($newAuthEvent); $bearerAuthBackend = new BearerAuth( - \OC::$server->getUserSession(), - \OC::$server->getSession(), - \OC::$server->getRequest() + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(ISession::class), + \OCP\Server::get(IRequest::class), + \OCP\Server::get(IConfig::class), ); $authPlugin->addBackend($bearerAuthBackend); // because we are throwing exceptions this plugin has to be the last one $authPlugin->addBackend($authBackend); // debugging - if (\OC::$server->getConfig()->getSystemValue('debug', false)) { + if ($debugEnabled) { + $this->server->debugEnabled = true; + $this->server->addPlugin(new PropFindMonitorPlugin()); $this->server->addPlugin(new \Sabre\DAV\Browser\Plugin()); } else { $this->server->addPlugin(new DummyGetResponsePlugin()); } - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger)); - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin()); + $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger)); + $this->server->addPlugin(new LockPlugin()); $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); // acl @@ -176,64 +194,69 @@ class Server { // calendar plugins if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) { + $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class))); $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); - $this->server->addPlugin(new \OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin(\OC::$server->getConfig(), $logger)); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class))); - if (\OC::$server->getConfig()->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') { - $this->server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); - } + $this->server->addPlugin(new ICSExportPlugin(\OCP\Server::get(IConfig::class), $logger)); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OCP\Server::get(IConfig::class), \OCP\Server::get(LoggerInterface::class), \OCP\Server::get(DefaultCalendarValidator::class))); - $this->server->addPlugin(\OC::$server->get(\OCA\DAV\CalDAV\Trashbin\Plugin::class)); - $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($request)); - if (\OC::$server->getConfig()->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') { + $this->server->addPlugin(\OCP\Server::get(\OCA\DAV\CalDAV\Trashbin\Plugin::class)); + $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($this->request)); + if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') { $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); } $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); - $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig())); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin( - \OC::$server->getConfig(), - \OC::$server->getURLGenerator() + $this->server->addPlugin(new PublishPlugin( + \OCP\Server::get(IConfig::class), + \OCP\Server::get(IURLGenerator::class) )); $this->server->addPlugin(\OCP\Server::get(RateLimitingPlugin::class)); + $this->server->addPlugin(\OCP\Server::get(CalDavValidatePlugin::class)); } // addressbook plugins if ($this->requestIsForSubtree(['addressbooks', 'principals'])) { - $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig())); + $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class))); $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); $this->server->addPlugin(new VCFExportPlugin()); $this->server->addPlugin(new MultiGetExportPlugin()); $this->server->addPlugin(new HasPhotoPlugin()); - $this->server->addPlugin(new ImageExportPlugin(new PhotoCache( - \OC::$server->getAppDataDir('dav-photocache'), - $logger) - )); + $this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class))); + + $this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class)); + $this->server->addPlugin(\OCP\Server::get(CardDavValidatePlugin::class)); } // system tags plugins - $this->server->addPlugin(\OC::$server->get(SystemTagPlugin::class)); + $this->server->addPlugin(\OCP\Server::get(SystemTagPlugin::class)); // comments plugin $this->server->addPlugin(new CommentsPlugin( - \OC::$server->getCommentsManager(), - \OC::$server->getUserSession() + \OCP\Server::get(ICommentsManager::class), + \OCP\Server::get(IUserSession::class) )); $this->server->addPlugin(new CopyEtagHeaderPlugin()); - $this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class))); + $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class))); + $this->server->addPlugin(new UploadAutoMkcolPlugin()); $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class))); $this->server->addPlugin(new ChunkingPlugin()); + $this->server->addPlugin(new ZipFolderPlugin( + $this->server->tree, + $logger, + $eventDispatcher, + )); + $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class)); // allow setup of additional plugins - $dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); + $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); $typedEvent = new SabrePluginAddEvent($this->server); - $dispatcher->dispatchTyped($typedEvent); + $eventDispatcher->dispatchTyped($typedEvent); // Some WebDAV clients do require Class 2 WebDAV support (locking), since // we do not provide locking we emulate it using a fake locking plugin. - if ($request->isUserAgent([ + if ($this->request->isUserAgent([ '/WebDAVFS/', '/OneNote/', '/^Microsoft-WebDAV/',// Microsoft-WebDAV-MiniRedir/6.1.7601 @@ -249,26 +272,29 @@ class Server { $this->server->addPlugin(new SearchPlugin($lazySearchBackend)); // wait with registering these until auth is handled and the filesystem is setup - $this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger) { + $this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger): void { // Allow view-only plugin for webdav requests $this->server->addPlugin(new ViewOnlyPlugin( \OC::$server->getUserFolder(), )); // custom properties plugin must be the last one - $userSession = \OC::$server->getUserSession(); + $userSession = \OCP\Server::get(IUserSession::class); $user = $userSession->getUser(); if ($user !== null) { - $view = \OC\Files\Filesystem::getView(); + $view = Filesystem::getView(); + $config = \OCP\Server::get(IConfig::class); $this->server->addPlugin( new FilesPlugin( $this->server->tree, - \OC::$server->getConfig(), + $config, $this->request, - \OC::$server->getPreviewManager(), - \OC::$server->getUserSession(), + \OCP\Server::get(IPreview::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IFilenameValidator::class), + \OCP\Server::get(IAccountManager::class), false, - !\OC::$server->getConfig()->getSystemValue('debug', false) + $config->getSystemValueBool('debug', false) === false, ) ); $this->server->addPlugin(new ChecksumUpdatePlugin()); @@ -278,8 +304,9 @@ class Server { new CustomPropertiesBackend( $this->server, $this->server->tree, - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserSession()->getUser() + \OCP\Server::get(IDBConnection::class), + \OCP\Server::get(IUserSession::class)->getUser(), + \OCP\Server::get(DefaultCalendarValidator::class), ) ) ); @@ -289,39 +316,55 @@ class Server { } $this->server->addPlugin( new TagsPlugin( - $this->server->tree, \OC::$server->getTagManager() + $this->server->tree, \OCP\Server::get(ITagManager::class), \OCP\Server::get(IEventDispatcher::class), \OCP\Server::get(IUserSession::class) ) ); + // TODO: switch to LazyUserFolder $userFolder = \OC::$server->getUserFolder(); + $shareManager = \OCP\Server::get(\OCP\Share\IManager::class); $this->server->addPlugin(new SharesPlugin( $this->server->tree, $userSession, $userFolder, - \OC::$server->getShareManager() + $shareManager, )); $this->server->addPlugin(new CommentPropertiesPlugin( - \OC::$server->getCommentsManager(), + \OCP\Server::get(ICommentsManager::class), $userSession )); + if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') { + $this->server->addPlugin(new IMipPlugin( + \OCP\Server::get(IAppConfig::class), + \OCP\Server::get(IMailer::class), + \OCP\Server::get(LoggerInterface::class), + \OCP\Server::get(ITimeFactory::class), + \OCP\Server::get(Defaults::class), + $userSession, + \OCP\Server::get(IMipService::class), + \OCP\Server::get(EventComparisonService::class), + \OCP\Server::get(\OCP\Mail\Provider\IManager::class) + )); + } $this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin()); if ($view !== null) { $this->server->addPlugin(new FilesReportPlugin( $this->server->tree, $view, - \OC::$server->getSystemTagManager(), - \OC::$server->getSystemTagObjectMapper(), - \OC::$server->getTagManager(), + \OCP\Server::get(ISystemTagManager::class), + \OCP\Server::get(ISystemTagObjectMapper::class), + \OCP\Server::get(ITagManager::class), $userSession, - \OC::$server->getGroupManager(), + \OCP\Server::get(IGroupManager::class), $userFolder, - \OC::$server->getAppManager() + \OCP\Server::get(IAppManager::class) )); - $lazySearchBackend->setBackend(new \OCA\DAV\Files\FileSearchBackend( + $lazySearchBackend->setBackend(new FileSearchBackend( + $this->server, $this->server->tree, $user, - \OC::$server->getRootFolder(), - \OC::$server->getShareManager(), + \OCP\Server::get(IRootFolder::class), + $shareManager, $view, \OCP\Server::get(IFilesMetadataManager::class) )); @@ -332,16 +375,16 @@ class Server { ) ); } - $this->server->addPlugin(new \OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin( - \OC::$server->getConfig(), - \OC::$server->query(BirthdayService::class), + $this->server->addPlugin(new EnablePlugin( + \OCP\Server::get(IConfig::class), + \OCP\Server::get(BirthdayService::class), $user )); $this->server->addPlugin(new AppleProvisioningPlugin( - \OC::$server->getUserSession(), - \OC::$server->getURLGenerator(), - \OC::$server->getThemingDefaults(), - \OC::$server->getRequest(), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IURLGenerator::class), + \OCP\Server::get(ThemingDefaults::class), + \OCP\Server::get(IRequest::class), \OC::$server->getL10N('dav'), function () { return UUIDUtil::getUUID(); @@ -352,7 +395,7 @@ class Server { // register plugins from apps $pluginManager = new PluginManager( \OC::$server, - \OC::$server->getAppManager() + \OCP\Server::get(IAppManager::class) ); foreach ($pluginManager->getAppPlugins() as $appPlugin) { $this->server->addPlugin($appPlugin); @@ -369,13 +412,13 @@ class Server { public function exec() { /** @var IEventLogger $eventLogger */ - $eventLogger = \OC::$server->get(IEventLogger::class); + $eventLogger = \OCP\Server::get(IEventLogger::class); $eventLogger->start('dav_server_exec', ''); - $this->server->exec(); + $this->server->start(); $eventLogger->end('dav_server_exec'); if ($this->profiler->isEnabled()) { $eventLogger->end('runtime'); - $profile = $this->profiler->collect(\OC::$server->get(IRequest::class), new Response()); + $profile = $this->profiler->collect(\OCP\Server::get(IRequest::class), new Response()); $this->profiler->saveProfile($profile); } } diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php index 7dc74f7d6ae..f632ee6015d 100644 --- a/apps/dav/lib/ServerFactory.php +++ b/apps/dav/lib/ServerFactory.php @@ -3,33 +3,22 @@ declare(strict_types=1); /** - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV; use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\Connector\Sabre\Server; class ServerFactory { public function createInviationResponseServer(bool $public): InvitationResponseServer { return new InvitationResponseServer(false); } + + public function createAttendeeAvailabilityServer(): Server { + return (new InvitationResponseServer(false))->getServer(); + } } diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 3e2a218d52b..7cbc0386d43 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> - * - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Service; @@ -64,6 +47,8 @@ class AbsenceService { string $lastDay, string $status, string $message, + ?string $replacementUserId = null, + ?string $replacementUserDisplayName = null, ): Absence { try { $absence = $this->absenceMapper->findByUserId($user->getUID()); @@ -76,6 +61,8 @@ class AbsenceService { $absence->setLastDay($lastDay); $absence->setStatus($status); $absence->setMessage($message); + $absence->setReplacementUserId($replacementUserId); + $absence->setReplacementUserDisplayName($replacementUserDisplayName); if ($absence->getId() === null) { $absence = $this->absenceMapper->insert($absence); diff --git a/apps/dav/lib/Service/ExampleContactService.php b/apps/dav/lib/Service/ExampleContactService.php new file mode 100644 index 00000000000..6ed6c66cbb3 --- /dev/null +++ b/apps/dav/lib/Service/ExampleContactService.php @@ -0,0 +1,132 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Service; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\AppFramework\Services\IAppConfig; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; + +class ExampleContactService { + private readonly IAppData $appData; + + public function __construct( + IAppDataFactory $appDataFactory, + private readonly IAppConfig $appConfig, + private readonly LoggerInterface $logger, + private readonly CardDavBackend $cardDav, + ) { + $this->appData = $appDataFactory->get(Application::APP_ID); + } + + public function isDefaultContactEnabled(): bool { + return $this->appConfig->getAppValueBool('enableDefaultContact', true); + } + + public function setDefaultContactEnabled(bool $value): void { + $this->appConfig->setAppValueBool('enableDefaultContact', $value); + } + + public function getCard(): ?string { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + return null; + } + + if (!$folder->fileExists('defaultContact.vcf')) { + return null; + } + + return $folder->getFile('defaultContact.vcf')->getContent(); + } + + public function setCard(?string $cardData = null) { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('defaultContact'); + } + + $isCustom = true; + if (is_null($cardData)) { + $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); + $isCustom = false; + } + + if (!$cardData) { + throw new \Exception('Could not read exampleContact.vcf'); + } + + $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); + $file->putContent($cardData); + + $this->appConfig->setAppValueBool('hasCustomDefaultContact', $isCustom); + } + + public function defaultContactExists(): bool { + try { + $folder = $this->appData->getFolder('defaultContact'); + } catch (NotFoundException $e) { + return false; + } + return $folder->fileExists('defaultContact.vcf'); + } + + public function createDefaultContact(int $addressBookId): void { + if (!$this->isDefaultContactEnabled()) { + return; + } + + try { + $folder = $this->appData->getFolder('defaultContact'); + $defaultContactFile = $folder->getFile('defaultContact.vcf'); + $data = $defaultContactFile->getContent(); + } catch (\Exception $e) { + $this->logger->error('Couldn\'t get default contact file', ['exception' => $e]); + return; + } + + // Make sure the UID is unique + $newUid = Uuid::v4()->toRfc4122(); + $newRev = date('Ymd\THis\Z'); + $vcard = \Sabre\VObject\Reader::read($data, \Sabre\VObject\Reader::OPTION_FORGIVING); + if ($vcard->UID) { + $vcard->UID->setValue($newUid); + } else { + $vcard->add('UID', $newUid); + } + if ($vcard->REV) { + $vcard->REV->setValue($newRev); + } else { + $vcard->add('REV', $newRev); + } + + // Level 3 means that the document is invalid + // https://sabre.io/vobject/vcard/#validating-vcard + $level3Warnings = array_filter($vcard->validate(), static function ($warning) { + return $warning['level'] === 3; + }); + + if (!empty($level3Warnings)) { + $this->logger->error('Default contact is invalid', ['warnings' => $level3Warnings]); + return; + } + try { + $this->cardDav->createCard($addressBookId, 'default', $vcard->serialize(), false); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } +} diff --git a/apps/dav/lib/Service/ExampleEventService.php b/apps/dav/lib/Service/ExampleEventService.php new file mode 100644 index 00000000000..3b2b07fe416 --- /dev/null +++ b/apps/dav/lib/Service/ExampleEventService.php @@ -0,0 +1,205 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Service; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Exception\ExampleEventException; +use OCA\DAV\Model\ExampleEvent; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IAppConfig; +use OCP\IL10N; +use OCP\Security\ISecureRandom; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; + +class ExampleEventService { + private const FOLDER_NAME = 'example_event'; + private const FILE_NAME = 'example_event.ics'; + private const ENABLE_CONFIG_KEY = 'create_example_event'; + + public function __construct( + private readonly CalDavBackend $calDavBackend, + private readonly ISecureRandom $random, + private readonly ITimeFactory $time, + private readonly IAppData $appData, + private readonly IAppConfig $appConfig, + private readonly IL10N $l10n, + ) { + } + + public function createExampleEvent(int $calendarId): void { + if (!$this->shouldCreateExampleEvent()) { + return; + } + + $exampleEvent = $this->getExampleEvent(); + $uid = $exampleEvent->getUid(); + $this->calDavBackend->createCalendarObject( + $calendarId, + "$uid.ics", + $exampleEvent->getIcs(), + ); + } + + private function getStartDate(): \DateTimeInterface { + return $this->time->now() + ->add(new \DateInterval('P7D')) + ->setTime(10, 00); + } + + private function getEndDate(): \DateTimeInterface { + return $this->time->now() + ->add(new \DateInterval('P7D')) + ->setTime(11, 00); + } + + private function getDefaultEvent(string $uid): VCalendar { + $defaultDescription = $this->l10n->t(<<<EOF +Welcome to Nextcloud Calendar! + +This is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want! + +With Nextcloud Calendar, you can: +- Create, edit, and manage events effortlessly. +- Create multiple calendars and share them with teammates, friends, or family. +- Check availability and display your busy times to others. +- Seamlessly integrate with apps and devices via CalDAV. +- Customize your experience: schedule recurring events, adjust notifications and other settings. +EOF); + + $vCalendar = new VCalendar(); + $props = [ + 'UID' => $uid, + 'DTSTAMP' => $this->time->now(), + 'SUMMARY' => $this->l10n->t('Example event - open me!'), + 'DTSTART' => $this->getStartDate(), + 'DTEND' => $this->getEndDate(), + 'DESCRIPTION' => $defaultDescription, + ]; + $vCalendar->add('VEVENT', $props); + return $vCalendar; + } + + /** + * @return string|null The ics of the custom example event or null if no custom event was uploaded. + * @throws ExampleEventException If reading the custom ics file fails. + */ + private function getCustomExampleEvent(): ?string { + try { + $folder = $this->appData->getFolder(self::FOLDER_NAME); + $icsFile = $folder->getFile(self::FILE_NAME); + } catch (NotFoundException $e) { + return null; + } + + try { + return $icsFile->getContent(); + } catch (NotFoundException|NotPermittedException $e) { + throw new ExampleEventException( + 'Failed to read custom example event', + 0, + $e, + ); + } + } + + /** + * Get the configured example event or the default one. + * + * @throws ExampleEventException If loading the custom example event fails. + */ + public function getExampleEvent(): ExampleEvent { + $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC); + $customIcs = $this->getCustomExampleEvent(); + if ($customIcs === null) { + return new ExampleEvent($this->getDefaultEvent($uid), $uid); + } + + [$vCalendar, $vEvent] = $this->parseEvent($customIcs); + $vEvent->UID = $uid; + $vEvent->DTSTART = $this->getStartDate(); + $vEvent->DTEND = $this->getEndDate(); + $vEvent->remove('ORGANIZER'); + $vEvent->remove('ATTENDEE'); + return new ExampleEvent($vCalendar, $uid); + } + + /** + * @psalm-return list{VCalendar, VEvent} The VCALENDAR document and its VEVENT child component + * @throws ExampleEventException If parsing the event fails or if it is invalid. + */ + private function parseEvent(string $ics): array { + try { + $vCalendar = \Sabre\VObject\Reader::read($ics); + if (!($vCalendar instanceof VCalendar)) { + throw new ExampleEventException('Custom event does not contain a VCALENDAR component'); + } + + /** @var VEvent|null $vEvent */ + $vEvent = $vCalendar->getBaseComponent('VEVENT'); + if ($vEvent === null) { + throw new ExampleEventException('Custom event does not contain a VEVENT component'); + } + } catch (\Exception $e) { + throw new ExampleEventException('Failed to parse custom event: ' . $e->getMessage(), 0, $e); + } + + return [$vCalendar, $vEvent]; + } + + public function saveCustomExampleEvent(string $ics): void { + // Parse and validate the event before attempting to save it to prevent run time errors + $this->parseEvent($ics); + + try { + $folder = $this->appData->getFolder(self::FOLDER_NAME); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder(self::FOLDER_NAME); + } + + try { + $existingFile = $folder->getFile(self::FILE_NAME); + $existingFile->putContent($ics); + } catch (NotFoundException $e) { + $folder->newFile(self::FILE_NAME, $ics); + } + } + + public function deleteCustomExampleEvent(): void { + try { + $folder = $this->appData->getFolder(self::FOLDER_NAME); + $file = $folder->getFile(self::FILE_NAME); + } catch (NotFoundException $e) { + return; + } + + $file->delete(); + } + + public function hasCustomExampleEvent(): bool { + try { + return $this->getCustomExampleEvent() !== null; + } catch (ExampleEventException $e) { + return false; + } + } + + public function setCreateExampleEvent(bool $enable): void { + $this->appConfig->setValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, $enable); + } + + public function shouldCreateExampleEvent(): bool { + return $this->appConfig->getValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, true); + } +} diff --git a/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php b/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php new file mode 100644 index 00000000000..2f7b9f8fcc9 --- /dev/null +++ b/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Settings\Admin; + +use OCP\IL10N; +use OCP\Settings\DeclarativeSettingsTypes; +use OCP\Settings\IDeclarativeSettingsForm; + +class SystemAddressBookSettings implements IDeclarativeSettingsForm { + + public function __construct( + private IL10N $l, + ) { + } + + public function getSchema(): array { + return [ + 'id' => 'dav-admin-system-address-book', + 'priority' => 10, + 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, + 'section_id' => 'groupware', + 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, + 'title' => $this->l->t('System Address Book'), + 'description' => $this->l->t('The system address book contains contact information for all users in your instance.'), + + 'fields' => [ + [ + 'id' => 'system_addressbook_enabled', + 'title' => $this->l->t('Enable System Address Book'), + 'type' => DeclarativeSettingsTypes::CHECKBOX, + 'default' => false, + 'options' => [], + ], + ], + ]; + } + +} diff --git a/apps/dav/lib/Settings/AvailabilitySettings.php b/apps/dav/lib/Settings/AvailabilitySettings.php index f8986ffe5d1..a1ada96b255 100644 --- a/apps/dav/lib/Settings/AvailabilitySettings.php +++ b/apps/dav/lib/Settings/AvailabilitySettings.php @@ -2,26 +2,9 @@ declare(strict_types=1); -/* - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Settings; @@ -37,19 +20,14 @@ use OCP\User\IAvailabilityCoordinator; use Psr\Log\LoggerInterface; class AvailabilitySettings implements ISettings { - protected IConfig $config; - protected IInitialState $initialState; - protected ?string $userId; - - public function __construct(IConfig $config, - IInitialState $initialState, - ?string $userId, + public function __construct( + protected IConfig $config, + protected IInitialState $initialState, + protected ?string $userId, private LoggerInterface $logger, private IAvailabilityCoordinator $coordinator, - private AbsenceMapper $absenceMapper) { - $this->config = $config; - $this->initialState = $initialState; - $this->userId = $userId; + private AbsenceMapper $absenceMapper, + ) { } public function getForm(): TemplateResponse { diff --git a/apps/dav/lib/Settings/CalDAVSettings.php b/apps/dav/lib/Settings/CalDAVSettings.php index 2985c9fc888..5e19539a899 100644 --- a/apps/dav/lib/Settings/CalDAVSettings.php +++ b/apps/dav/lib/Settings/CalDAVSettings.php @@ -1,31 +1,13 @@ <?php + /** - * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author François Freitag <mail@franek.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Settings; use OCA\DAV\AppInfo\Application; +use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\IConfig; @@ -34,14 +16,6 @@ use OCP\Settings\IDelegatedSettings; class CalDAVSettings implements IDelegatedSettings { - /** @var IConfig */ - private $config; - - /** @var IInitialState */ - private $initialState; - - private IURLGenerator $urlGenerator; - private const defaults = [ 'sendInvitations' => 'yes', 'generateBirthdayCalendar' => 'yes', @@ -56,10 +30,12 @@ class CalDAVSettings implements IDelegatedSettings { * @param IConfig $config * @param IInitialState $initialState */ - public function __construct(IConfig $config, IInitialState $initialState, IURLGenerator $urlGenerator) { - $this->config = $config; - $this->initialState = $initialState; - $this->urlGenerator = $urlGenerator; + public function __construct( + private IConfig $config, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + private IAppManager $appManager, + ) { } public function getForm(): TemplateResponse { @@ -71,10 +47,11 @@ class CalDAVSettings implements IDelegatedSettings { return new TemplateResponse(Application::APP_ID, 'settings-admin-caldav'); } - /** - * @return string - */ - public function getSection() { + public function getSection(): ?string { + if (!$this->appManager->isBackendRequired(IAppManager::BACKEND_CALDAV)) { + return null; + } + return 'groupware'; } diff --git a/apps/dav/lib/Settings/ExampleContentSettings.php b/apps/dav/lib/Settings/ExampleContentSettings.php new file mode 100644 index 00000000000..7b6f9b03a3a --- /dev/null +++ b/apps/dav/lib/Settings/ExampleContentSettings.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Settings; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\Service\ExampleContactService; +use OCA\DAV\Service\ExampleEventService; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IAppConfig; +use OCP\AppFramework\Services\IInitialState; +use OCP\Settings\ISettings; + +class ExampleContentSettings implements ISettings { + public function __construct( + private readonly IAppConfig $appConfig, + private readonly IInitialState $initialState, + private readonly IAppManager $appManager, + private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, + ) { + } + + public function getForm(): TemplateResponse { + $calendarEnabled = $this->appManager->isEnabledForUser('calendar'); + $contactsEnabled = $this->appManager->isEnabledForUser('contacts'); + $this->initialState->provideInitialState('calendarEnabled', $calendarEnabled); + $this->initialState->provideInitialState('contactsEnabled', $contactsEnabled); + + if ($calendarEnabled) { + $enableDefaultEvent = $this->exampleEventService->shouldCreateExampleEvent(); + $this->initialState->provideInitialState('create_example_event', $enableDefaultEvent); + $this->initialState->provideInitialState( + 'has_custom_example_event', + $this->exampleEventService->hasCustomExampleEvent(), + ); + } + + if ($contactsEnabled) { + $this->initialState->provideInitialState( + 'enableDefaultContact', + $this->exampleContactService->isDefaultContactEnabled(), + ); + $this->initialState->provideInitialState( + 'hasCustomDefaultContact', + $this->appConfig->getAppValueBool('hasCustomDefaultContact'), + ); + } + + return new TemplateResponse(Application::APP_ID, 'settings-example-content'); + } + + public function getSection(): ?string { + if (!$this->appManager->isEnabledForUser('contacts') + && !$this->appManager->isEnabledForUser('calendar')) { + return null; + } + + return 'groupware'; + } + + public function getPriority(): int { + return 10; + } +} diff --git a/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php b/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php index 32c6fb06393..c3f0742a640 100644 --- a/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php +++ b/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Anna Larch <anna.larch@gmx.net> - * - * @author Anna Larch <anna.larch@gmx.net> - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\SetupChecks; diff --git a/apps/dav/lib/SetupChecks/WebdavEndpoint.php b/apps/dav/lib/SetupChecks/WebdavEndpoint.php index 72585e93eeb..c2574202fcd 100644 --- a/apps/dav/lib/SetupChecks/WebdavEndpoint.php +++ b/apps/dav/lib/SetupChecks/WebdavEndpoint.php @@ -3,35 +3,17 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2024 Côme Chilliet <come.chilliet@nextcloud.com> - * - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\SetupChecks; -use OCA\Settings\SetupChecks\CheckServerResponseTrait; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; +use OCP\SetupCheck\CheckServerResponseTrait; use OCP\SetupCheck\ISetupCheck; use OCP\SetupCheck\SetupResult; use Psr\Log\LoggerInterface; diff --git a/apps/dav/lib/Storage/PublicOwnerWrapper.php b/apps/dav/lib/Storage/PublicOwnerWrapper.php index 10bcd20de05..a0f1607d971 100644 --- a/apps/dav/lib/Storage/PublicOwnerWrapper.php +++ b/apps/dav/lib/Storage/PublicOwnerWrapper.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Storage; @@ -29,27 +12,25 @@ use OC\Files\Storage\Wrapper\Wrapper; class PublicOwnerWrapper extends Wrapper { - /** @var string */ - private $owner; + private string $owner; /** - * @param array $arguments ['storage' => $storage, 'owner' => $owner] + * @param array $parameters ['storage' => $storage, 'owner' => $owner] * * $storage: The storage the permissions mask should be applied on * $owner: The owner to use in case no owner is found */ - public function __construct($arguments) { - parent::__construct($arguments); - $this->owner = $arguments['owner']; + public function __construct(array $parameters) { + parent::__construct($parameters); + $this->owner = $parameters['owner']; } - public function getOwner($path) { + public function getOwner(string $path): string|false { $owner = parent::getOwner($path); - - if ($owner === null || $owner === false) { - return $this->owner; + if ($owner !== false) { + return $owner; } - return $owner; + return $this->owner; } } diff --git a/apps/dav/lib/Storage/PublicShareWrapper.php b/apps/dav/lib/Storage/PublicShareWrapper.php new file mode 100644 index 00000000000..fb0db4dca4c --- /dev/null +++ b/apps/dav/lib/Storage/PublicShareWrapper.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Storage; + +use OC\Files\Storage\Wrapper\Wrapper; +use OCP\Files\Storage\ISharedStorage; +use OCP\Share\IShare; + +class PublicShareWrapper extends Wrapper implements ISharedStorage { + + private IShare $share; + + /** + * @param array $parameters ['storage' => $storage, 'share' => $share] + * + * $storage: The storage the permissions mask should be applied on + * $share: The share to use in case no share is found + */ + public function __construct(array $parameters) { + parent::__construct($parameters); + $this->share = $parameters['share']; + } + + public function getShare(): IShare { + $storage = parent::getWrapperStorage(); + if (method_exists($storage, 'getShare')) { + /** @var ISharedStorage $storage */ + return $storage->getShare(); + } + + return $this->share; + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagList.php b/apps/dav/lib/SystemTag/SystemTagList.php index 678c8042a39..b55f10164d7 100644 --- a/apps/dav/lib/SystemTag/SystemTagList.php +++ b/apps/dav/lib/SystemTag/SystemTagList.php @@ -1,22 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\SystemTag; @@ -34,16 +20,20 @@ use Sabre\Xml\Writer; */ class SystemTagList implements Element { public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; + private array $canAssignTagMap = []; - /** @var ISystemTag[] */ - private array $tags; - private ISystemTagManager $tagManager; - private IUser $user; - - public function __construct(array $tags, ISystemTagManager $tagManager, IUser $user) { + /** + * @param ISystemTag[] $tags + */ + public function __construct( + private array $tags, + ISystemTagManager $tagManager, + ?IUser $user, + ) { $this->tags = $tags; - $this->tagManager = $tagManager; - $this->user = $user; + foreach ($this->tags as $tag) { + $this->canAssignTagMap[$tag->getId()] = $tagManager->canUserAssignTag($tag, $user); + } } /** @@ -61,10 +51,11 @@ class SystemTagList implements Element { foreach ($this->tags as $tag) { $writer->startElement('{' . self::NS_NEXTCLOUD . '}system-tag'); $writer->writeAttributes([ - SystemTagPlugin::CANASSIGN_PROPERTYNAME => $this->tagManager->canUserAssignTag($tag, $this->user) ? 'true' : 'false', + SystemTagPlugin::CANASSIGN_PROPERTYNAME => $this->canAssignTagMap[$tag->getId()] ? 'true' : 'false', SystemTagPlugin::ID_PROPERTYNAME => $tag->getId(), SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME => $tag->isUserAssignable() ? 'true' : 'false', SystemTagPlugin::USERVISIBLE_PROPERTYNAME => $tag->isUserVisible() ? 'true' : 'false', + SystemTagPlugin::COLOR_PROPERTYNAME => $tag->getColor() ?? '', ]); $writer->write($tag->getName()); $writer->endElement(); diff --git a/apps/dav/lib/SystemTag/SystemTagMappingNode.php b/apps/dav/lib/SystemTag/SystemTagMappingNode.php index 113c8d82836..12d604a7d6e 100644 --- a/apps/dav/lib/SystemTag/SystemTagMappingNode.php +++ b/apps/dav/lib/SystemTag/SystemTagMappingNode.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php index 8ade5085b03..2341d4823ba 100644 --- a/apps/dav/lib/SystemTag/SystemTagNode.php +++ b/apps/dav/lib/SystemTag/SystemTagNode.php @@ -1,35 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; use OCP\IUser; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagAlreadyExistsException; - use OCP\SystemTag\TagNotFoundException; +use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Conflict; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\MethodNotAllowed; @@ -38,31 +22,7 @@ use Sabre\DAV\Exception\NotFound; /** * DAV node representing a system tag, with the name being the tag id. */ -class SystemTagNode implements \Sabre\DAV\INode { - - /** - * @var ISystemTag - */ - protected $tag; - - /** - * @var ISystemTagManager - */ - protected $tagManager; - - /** - * User - * - * @var IUser - */ - protected $user; - - /** - * Whether to allow permissions for admins - * - * @var bool - */ - protected $isAdmin; +class SystemTagNode implements \Sabre\DAV\ICollection { protected int $numberOfFiles = -1; protected int $referenceFileId = -1; @@ -75,11 +35,19 @@ class SystemTagNode implements \Sabre\DAV\INode { * @param bool $isAdmin whether to allow operations for admins * @param ISystemTagManager $tagManager tag manager */ - public function __construct(ISystemTag $tag, IUser $user, $isAdmin, ISystemTagManager $tagManager) { - $this->tag = $tag; - $this->user = $user; - $this->isAdmin = $isAdmin; - $this->tagManager = $tagManager; + public function __construct( + protected ISystemTag $tag, + /** + * User + */ + protected IUser $user, + /** + * Whether to allow permissions for admins + */ + protected bool $isAdmin, + protected ISystemTagManager $tagManager, + protected ISystemTagObjectMapper $tagMapper, + ) { } /** @@ -119,12 +87,13 @@ class SystemTagNode implements \Sabre\DAV\INode { * @param string $name new tag name * @param bool $userVisible user visible * @param bool $userAssignable user assignable + * @param string $color color * * @throws NotFound whenever the given tag id does not exist * @throws Forbidden whenever there is no permission to update said tag * @throws Conflict whenever a tag already exists with the given attributes */ - public function update($name, $userVisible, $userAssignable): void { + public function update($name, $userVisible, $userAssignable, $color): void { try { if (!$this->tagManager->canUserSeeTag($this->tag, $this->user)) { throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); @@ -143,13 +112,18 @@ class SystemTagNode implements \Sabre\DAV\INode { } } - $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable); + // Make sure color is a proper hex + if ($color !== null && (strlen($color) !== 6 || !ctype_xdigit($color))) { + throw new BadRequest('Color must be a 6-digit hexadecimal value'); + } + + $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable, $color); } catch (TagNotFoundException $e) { throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); } catch (TagAlreadyExistsException $e) { throw new Conflict( - 'Tag with the properties "' . $name . '", ' . - $userVisible . ', ' . $userAssignable . ' already exists' + 'Tag with the properties "' . $name . '", ' + . $userVisible . ', ' . $userAssignable . ' already exists' ); } } @@ -198,4 +172,31 @@ class SystemTagNode implements \Sabre\DAV\INode { public function setReferenceFileId(int $referenceFileId): void { $this->referenceFileId = $referenceFileId; } + + public function createFile($name, $data = null) { + throw new MethodNotAllowed(); + } + + public function createDirectory($name) { + throw new MethodNotAllowed(); + } + + public function getChild($name) { + return new SystemTagObjectType($this->tag, $name, $this->tagManager, $this->tagMapper); + } + + public function childExists($name) { + $objectTypes = $this->tagMapper->getAvailableObjectTypes(); + return in_array($name, $objectTypes); + } + + public function getChildren() { + $objectTypes = $this->tagMapper->getAvailableObjectTypes(); + return array_map( + function ($objectType) { + return new SystemTagObjectType($this->tag, $objectType, $this->tagManager, $this->tagMapper); + }, + $objectTypes + ); + } } diff --git a/apps/dav/lib/SystemTag/SystemTagObjectType.php b/apps/dav/lib/SystemTag/SystemTagObjectType.php new file mode 100644 index 00000000000..0d348cd95f4 --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagObjectType.php @@ -0,0 +1,82 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\SystemTag; + +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use Sabre\DAV\Exception\MethodNotAllowed; + +/** + * SystemTagObjectType property + * This property represent a type of object which tags are assigned to. + */ +class SystemTagObjectType implements \Sabre\DAV\IFile { + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; + + /** @var string[] */ + private array $objectsIds = []; + + public function __construct( + private ISystemTag $tag, + private string $type, + private ISystemTagManager $tagManager, + private ISystemTagObjectMapper $tagMapper, + ) { + } + + /** + * Get the list of object ids that have this tag assigned. + */ + public function getObjectsIds(): array { + if (empty($this->objectsIds)) { + $this->objectsIds = $this->tagMapper->getObjectIdsForTags($this->tag->getId(), $this->type); + } + + return $this->objectsIds; + } + + /** + * Returns the system tag represented by this node + * + * @return ISystemTag system tag + */ + public function getSystemTag() { + return $this->tag; + } + + public function getName() { + return $this->type; + } + + public function getLastModified() { + return null; + } + + public function getETag() { + return '"' . $this->tag->getETag() . '"'; + } + + public function setName($name) { + throw new MethodNotAllowed(); + } + public function put($data) { + throw new MethodNotAllowed(); + } + public function get() { + throw new MethodNotAllowed(); + } + public function delete() { + throw new MethodNotAllowed(); + } + public function getContentType() { + throw new MethodNotAllowed(); + } + public function getSize() { + throw new MethodNotAllowed(); + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index 34b71ae40f2..4d4499c7559 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -1,32 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\DAV\Connector\Sabre\Node; +use OCP\AppFramework\Http; +use OCP\Constants; +use OCP\Files\IRootFolder; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserSession; @@ -34,6 +20,8 @@ use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagAlreadyExistsException; +use OCP\SystemTag\TagCreationForbiddenException; +use OCP\SystemTag\TagUpdateForbiddenException; use OCP\Util; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Conflict; @@ -55,6 +43,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { // namespace public const NS_OWNCLOUD = 'http://owncloud.org/ns'; + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id'; public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name'; public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible'; @@ -63,45 +52,27 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign'; public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags'; public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned'; - public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid'; + public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid'; + public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids'; + public const COLOR_PROPERTYNAME = '{http://nextcloud.org/ns}color'; /** * @var \Sabre\DAV\Server $server */ private $server; - /** - * @var ISystemTagManager - */ - protected $tagManager; - - /** - * @var IUserSession - */ - protected $userSession; - - /** - * @var IGroupManager - */ - protected $groupManager; - /** @var array<int, string[]> */ private array $cachedTagMappings = []; /** @var array<string, ISystemTag> */ private array $cachedTags = []; - private ISystemTagObjectMapper $tagMapper; - public function __construct( - ISystemTagManager $tagManager, - IGroupManager $groupManager, - IUserSession $userSession, - ISystemTagObjectMapper $tagMapper, + protected ISystemTagManager $tagManager, + protected IGroupManager $groupManager, + protected IUserSession $userSession, + protected IRootFolder $rootFolder, + protected ISystemTagObjectMapper $tagMapper, ) { - $this->tagManager = $tagManager; - $this->userSession = $userSession; - $this->groupManager = $groupManager; - $this->tagMapper = $tagMapper; } /** @@ -117,6 +88,9 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { */ public function initialize(\Sabre\DAV\Server $server) { $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; + $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc'; + + $server->xml->elementMap[self::OBJECTIDS_PROPERTYNAME] = SystemTagsObjectList::class; $server->protectedProperties[] = self::ID_PROPERTYNAME; @@ -159,7 +133,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $response->setHeader('Content-Location', $url . $tag->getId()); // created - $response->setStatus(201); + $response->setStatus(Http::STATUS_CREATED); return false; } } @@ -220,6 +194,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { return $tag; } catch (TagAlreadyExistsException $e) { throw new Conflict('Tag already exists', 0, $e); + } catch (TagCreationForbiddenException $e) { + throw new Forbidden('You don’t have permissions to create tags', 0, $e); } } @@ -234,14 +210,14 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { */ public function handleGetProperties( PropFind $propFind, - \Sabre\DAV\INode $node + \Sabre\DAV\INode $node, ) { if ($node instanceof Node) { $this->propfindForFile($propFind, $node); return; } - if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) { + if (!$node instanceof SystemTagNode && !$node instanceof SystemTagMappingNode && !$node instanceof SystemTagObjectType) { return; } @@ -250,6 +226,10 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath())); } + $propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string { + return '"' . ($node->getSystemTag()->getETag() ?? '') . '"'; + }); + $propFind->handle(self::ID_PROPERTYNAME, function () use ($node) { return $node->getSystemTag()->getId(); }); @@ -272,6 +252,10 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false'; }); + $propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) { + return $node->getSystemTag()->getColor() ?? ''; + }); + $propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) { if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { // property only available for admins @@ -290,9 +274,25 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { return $node->getNumberOfFiles(); }); - $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int { + $propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int { return $node->getReferenceFileId(); }); + + $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList { + $objectTypes = $this->tagMapper->getAvailableObjectTypes(); + $objects = []; + foreach ($objectTypes as $type) { + $systemTagObjectType = new SystemTagObjectType($node->getSystemTag(), $type, $this->tagManager, $this->tagMapper); + $objects = array_merge($objects, array_fill_keys($systemTagObjectType->getObjectsIds(), $type)); + } + return new SystemTagsObjectList($objects); + }); + } + + if ($node instanceof SystemTagObjectType) { + $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList { + return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName())); + }); } } @@ -323,9 +323,6 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) { $user = $this->userSession->getUser(); - if ($user === null) { - return; - } $tags = $this->getTagsForFile($node->getId(), $user); usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int { @@ -339,8 +336,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { * @param int $fileId * @return ISystemTag[] */ - private function getTagsForFile(int $fileId, IUser $user): array { - + private function getTagsForFile(int $fileId, ?IUser $user): array { if (isset($this->cachedTagMappings[$fileId])) { $tagIds = $this->cachedTagMappings[$fileId]; } else { @@ -384,22 +380,86 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { */ public function handleUpdateProperties($path, PropPatch $propPatch) { $node = $this->server->tree->getNodeForPath($path); - if (!($node instanceof SystemTagNode)) { + if (!$node instanceof SystemTagNode && !$node instanceof SystemTagObjectType) { return; } + $propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) { + if (!$node instanceof SystemTagObjectType) { + return false; + } + + if (isset($props[self::OBJECTIDS_PROPERTYNAME])) { + $user = $this->userSession->getUser(); + if (!$user) { + throw new Forbidden('You don’t have permissions to update tags'); + } + + $propValue = $props[self::OBJECTIDS_PROPERTYNAME]; + if (!$propValue instanceof SystemTagsObjectList || count($propValue->getObjects()) === 0) { + throw new BadRequest('Invalid object-ids property'); + } + + $objects = $propValue->getObjects(); + $objectTypes = array_unique(array_values($objects)); + + if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) { + throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName()); + } + + // Only files are supported at the moment + // Also see SystemTagsRelationsCollection file + if ($objectTypes[0] !== 'files') { + throw new BadRequest('Invalid object-ids property type. Only files are supported'); + } + + // Get all current tagged objects + $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files'); + $toAddObjects = array_map(fn ($value) => (string)$value, array_keys($objects)); + + // Compute the tags to add and remove + $addedObjects = array_values(array_diff($toAddObjects, $taggedObjects)); + $removedObjects = array_values(array_diff($taggedObjects, $toAddObjects)); + + // Check permissions for each object to be freshly tagged or untagged + if (!$this->canUpdateTagForFileIds(array_merge($addedObjects, $removedObjects))) { + throw new Forbidden('You don’t have permissions to update tags'); + } + + $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), array_keys($objects)); + } + + if ($props[self::OBJECTIDS_PROPERTYNAME] === null) { + // Check the user have permissions to remove the tag from all currently tagged objects + $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files'); + if (!$this->canUpdateTagForFileIds($taggedObjects)) { + throw new Forbidden('You don’t have permissions to update tags'); + } + + $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), []); + } + + return true; + }); + $propPatch->handle([ self::DISPLAYNAME_PROPERTYNAME, self::USERVISIBLE_PROPERTYNAME, self::USERASSIGNABLE_PROPERTYNAME, self::GROUPS_PROPERTYNAME, self::NUM_FILES_PROPERTYNAME, - self::FILEID_PROPERTYNAME, + self::REFERENCE_FILEID_PROPERTYNAME, + self::COLOR_PROPERTYNAME, ], function ($props) use ($node) { + if (!$node instanceof SystemTagNode) { + return false; + } + $tag = $node->getSystemTag(); $name = $tag->getName(); $userVisible = $tag->isUserVisible(); $userAssignable = $tag->isUserAssignable(); + $color = $tag->getColor(); $updateTag = false; @@ -420,6 +480,15 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $updateTag = true; } + if (isset($props[self::COLOR_PROPERTYNAME])) { + $propValue = $props[self::COLOR_PROPERTYNAME]; + if ($propValue === '' || $propValue === 'null') { + $propValue = null; + } + $color = $propValue; + $updateTag = true; + } + if (isset($props[self::GROUPS_PROPERTYNAME])) { if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) { // property only available for admins @@ -431,16 +500,40 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $this->tagManager->setTagGroups($tag, $groupIds); } - if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) { + if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::REFERENCE_FILEID_PROPERTYNAME])) { // read-only properties throw new Forbidden(); } if ($updateTag) { - $node->update($name, $userVisible, $userAssignable); + try { + $node->update($name, $userVisible, $userAssignable, $color); + } catch (TagUpdateForbiddenException $e) { + throw new Forbidden('You don’t have permissions to update tags', 0, $e); + } } return true; }); } + + /** + * Check if the user can update the tag for the given file ids + * + * @param list<string> $fileIds + * @return bool + */ + private function canUpdateTagForFileIds(array $fileIds): bool { + $user = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + foreach ($fileIds as $fileId) { + $nodes = $userFolder->getById((int)$fileId); + foreach ($nodes as $node) { + if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) { + return true; + } + } + } + return false; + } } diff --git a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php index 86ccadf5f56..b854db7b94d 100644 --- a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; @@ -28,6 +11,7 @@ use OCP\IGroupManager; use OCP\IUserSession; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagNotFoundException; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Forbidden; @@ -37,21 +21,6 @@ use Sabre\DAV\ICollection; class SystemTagsByIdCollection implements ICollection { /** - * @var ISystemTagManager - */ - private $tagManager; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var IUserSession - */ - private $userSession; - - /** * SystemTagsByIdCollection constructor. * * @param ISystemTagManager $tagManager @@ -59,13 +28,11 @@ class SystemTagsByIdCollection implements ICollection { * @param IGroupManager $groupManager */ public function __construct( - ISystemTagManager $tagManager, - IUserSession $userSession, - IGroupManager $groupManager + private ISystemTagManager $tagManager, + private IUserSession $userSession, + private IGroupManager $groupManager, + protected ISystemTagObjectMapper $tagMapper, ) { - $this->tagManager = $tagManager; - $this->userSession = $userSession; - $this->groupManager = $groupManager; } /** @@ -197,6 +164,6 @@ class SystemTagsByIdCollection implements ICollection { * @return SystemTagNode */ private function makeNode(ISystemTag $tag) { - return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager); + return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager, $this->tagMapper); } } diff --git a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php index 4ace9bde412..f11482b04ee 100644 --- a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\SystemTag; @@ -33,29 +16,23 @@ use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; use OCP\IUserSession; use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\SimpleCollection; class SystemTagsInUseCollection extends SimpleCollection { - protected IUserSession $userSession; - protected IRootFolder $rootFolder; - protected string $mediaType; - protected ISystemTagManager $systemTagManager; protected SystemTagsInFilesDetector $systemTagsInFilesDetector; /** @noinspection PhpMissingParentConstructorInspection */ public function __construct( - IUserSession $userSession, - IRootFolder $rootFolder, - ISystemTagManager $systemTagManager, + protected IUserSession $userSession, + protected IRootFolder $rootFolder, + protected ISystemTagManager $systemTagManager, + protected ISystemTagObjectMapper $tagMapper, SystemTagsInFilesDetector $systemTagsInFilesDetector, - string $mediaType = '' + protected string $mediaType = '', ) { - $this->userSession = $userSession; - $this->rootFolder = $rootFolder; - $this->systemTagManager = $systemTagManager; - $this->mediaType = $mediaType; $this->systemTagsInFilesDetector = $systemTagsInFilesDetector; $this->name = 'systemtags-assigned'; if ($this->mediaType != '') { @@ -71,7 +48,7 @@ class SystemTagsInUseCollection extends SimpleCollection { if ($this->mediaType !== '') { throw new NotFound('Invalid media type'); } - return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->systemTagsInFilesDetector, $name); + return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->tagMapper, $this->systemTagsInFilesDetector, $name); } /** @@ -96,11 +73,11 @@ class SystemTagsInUseCollection extends SimpleCollection { $result = $this->systemTagsInFilesDetector->detectAssignedSystemTagsIn($userFolder, $this->mediaType); $children = []; foreach ($result as $tagData) { - $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']); + $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable'], $tagData['etag'], $tagData['color']); // read only, so we can submit the isAdmin parameter as false generally - $node = new SystemTagNode($tag, $user, false, $this->systemTagManager); - $node->setNumberOfFiles((int) $tagData['number_files']); - $node->setReferenceFileId((int) $tagData['ref_file_id']); + $node = new SystemTagNode($tag, $user, false, $this->systemTagManager, $this->tagMapper); + $node->setNumberOfFiles((int)$tagData['number_files']); + $node->setReferenceFileId((int)$tagData['ref_file_id']); $children[] = $node; } return $children; diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectList.php b/apps/dav/lib/SystemTag/SystemTagsObjectList.php new file mode 100644 index 00000000000..64e8b1bbebb --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsObjectList.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\SystemTag; + +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\XmlSerializable; + +/** + * This property contains multiple "object-id" elements. + */ +class SystemTagsObjectList implements XmlSerializable, XmlDeserializable { + + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; + public const OBJECTID_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}object-id'; + public const OBJECTID_PROPERTYNAME = '{http://nextcloud.org/ns}id'; + public const OBJECTTYPE_PROPERTYNAME = '{http://nextcloud.org/ns}type'; + + /** + * @param array<string, string> $objects An array of object ids and their types + */ + public function __construct( + private array $objects, + ) { + } + + /** + * Get the object ids and their types. + * + * @return array<string, string> + */ + public function getObjects(): array { + return $this->objects; + } + + public static function xmlDeserialize(Reader $reader) { + $tree = $reader->parseInnerTree(); + if ($tree === null) { + return null; + } + + $objects = []; + foreach ($tree as $elem) { + if ($elem['name'] === self::OBJECTID_ROOT_PROPERTYNAME) { + $value = $elem['value']; + $id = ''; + $type = ''; + foreach ($value as $subElem) { + if ($subElem['name'] === self::OBJECTID_PROPERTYNAME) { + $id = $subElem['value']; + } elseif ($subElem['name'] === self::OBJECTTYPE_PROPERTYNAME) { + $type = $subElem['value']; + } + } + if ($id !== '' && $type !== '') { + $objects[(string)$id] = (string)$type; + } + } + } + + return new self($objects); + } + + /** + * The xmlSerialize method is called during xml writing. + * + * @param Writer $writer + * @return void + */ + public function xmlSerialize(Writer $writer) { + foreach ($this->objects as $objectsId => $type) { + $writer->startElement(SystemTagPlugin::OBJECTIDS_PROPERTYNAME); + $writer->writeElement(self::OBJECTID_PROPERTYNAME, $objectsId); + $writer->writeElement(self::OBJECTTYPE_PROPERTYNAME, $type); + $writer->endElement(); + } + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php index b45ef6c3f71..da58f9bf308 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php index f1d5fc06c99..9bd66ca0d61 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; diff --git a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php index 989c4640a61..0839a5bc995 100644 --- a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\SystemTag; @@ -47,6 +28,8 @@ class SystemTagsRelationsCollection extends SimpleCollection { IRootFolder $rootFolder, ) { $children = [ + // Only files are supported at the moment + // Also see SystemTagPlugin::OBJECTIDS_PROPERTYNAME supported types new SystemTagsObjectTypeCollection( 'files', $tagManager, diff --git a/apps/dav/lib/Traits/PrincipalProxyTrait.php b/apps/dav/lib/Traits/PrincipalProxyTrait.php index 6e764cac8c0..feec485fe5c 100644 --- a/apps/dav/lib/Traits/PrincipalProxyTrait.php +++ b/apps/dav/lib/Traits/PrincipalProxyTrait.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Traits; diff --git a/apps/dav/lib/Upload/AssemblyStream.php b/apps/dav/lib/Upload/AssemblyStream.php index ef6d39974c0..642a8604b17 100644 --- a/apps/dav/lib/Upload/AssemblyStream.php +++ b/apps/dav/lib/Upload/AssemblyStream.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Markus Goetz <markus@woboq.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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\Upload; @@ -96,6 +75,10 @@ class AssemblyStream implements \Icewind\Streams\File { $offset = $this->size + $offset; } + if ($offset === $this->pos) { + return true; + } + if ($offset > $this->size) { return false; } @@ -116,7 +99,7 @@ class AssemblyStream implements \Icewind\Streams\File { $stream = $this->getStream($this->nodes[$nodeIndex]); $nodeOffset = $offset - $nodeStart; - if (fseek($stream, $nodeOffset) === -1) { + if ($nodeOffset > 0 && fseek($stream, $nodeOffset) === -1) { return false; } $this->currentNode = $nodeIndex; @@ -147,9 +130,14 @@ class AssemblyStream implements \Icewind\Streams\File { } } - do { + $collectedData = ''; + // read data until we either got all the data requested or there is no more stream left + while ($count > 0 && !is_null($this->currentStream)) { $data = fread($this->currentStream, $count); $read = strlen($data); + + $count -= $read; + $collectedData .= $data; $this->currentNodeRead += $read; if (feof($this->currentStream)) { @@ -166,14 +154,11 @@ class AssemblyStream implements \Icewind\Streams\File { $this->currentStream = null; } } - // if no data read, try again with the next node because - // returning empty data can make the caller think there is no more - // data left to read - } while ($read === 0 && !is_null($this->currentStream)); + } // update position - $this->pos += $read; - return $data; + $this->pos += strlen($collectedData); + return $collectedData; } /** diff --git a/apps/dav/lib/Upload/ChunkingPlugin.php b/apps/dav/lib/Upload/ChunkingPlugin.php index 5a443ee7712..8cc8f7d6c61 100644 --- a/apps/dav/lib/Upload/ChunkingPlugin.php +++ b/apps/dav/lib/Upload/ChunkingPlugin.php @@ -1,32 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Upload; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCP\AppFramework\Http; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; @@ -107,7 +90,7 @@ class ChunkingPlugin extends ServerPlugin { $response = $this->server->httpResponse; $response->setHeader('Content-Length', '0'); - $response->setStatus($fileExists ? 204 : 201); + $response->setStatus($fileExists ? Http::STATUS_NO_CONTENT : Http::STATUS_CREATED); return false; } diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php index c66ffaa3f7d..07452dc0593 100644 --- a/apps/dav/lib/Upload/ChunkingV2Plugin.php +++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php @@ -1,26 +1,9 @@ <?php declare(strict_types=1); -/* - * @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Upload; @@ -35,6 +18,7 @@ use OC\Memcache\Redis; use OC_Hook; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\File; +use OCP\AppFramework\Http; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; @@ -141,7 +125,7 @@ class ChunkingV2Plugin extends ServerPlugin { self::UPLOAD_TARGET_ID => $targetFile->getId(), ], 86400); - $response->setStatus(201); + $response->setStatus(Http::STATUS_CREATED); return true; } @@ -252,7 +236,7 @@ class ChunkingV2Plugin extends ServerPlugin { $response = $this->server->httpResponse; $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); $response->setHeader('Content-Length', '0'); - $response->setStatus($destinationExists ? 204 : 201); + $response->setStatus($destinationExists ? Http::STATUS_NO_CONTENT : Http::STATUS_CREATED); return false; } diff --git a/apps/dav/lib/Upload/CleanupService.php b/apps/dav/lib/Upload/CleanupService.php index 2b6fc965c01..ffa6bad533c 100644 --- a/apps/dav/lib/Upload/CleanupService.php +++ b/apps/dav/lib/Upload/CleanupService.php @@ -3,48 +3,25 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Upload; use OCA\DAV\BackgroundJob\UploadCleanup; use OCP\BackgroundJob\IJobList; -use OCP\IUserSession; class CleanupService { - /** @var IUserSession */ - private $userSession; - /** @var IJobList */ - private $jobList; - - public function __construct(IUserSession $userSession, IJobList $jobList) { - $this->userSession = $userSession; - $this->jobList = $jobList; + public function __construct( + private IJobList $jobList, + ) { } - public function addJob(string $folder) { - $this->jobList->add(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]); + public function addJob(string $uid, string $folder) { + $this->jobList->add(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]); } - public function removeJob(string $folder) { - $this->jobList->remove(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]); + public function removeJob(string $uid, string $folder) { + $this->jobList->remove(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]); } } diff --git a/apps/dav/lib/Upload/FutureFile.php b/apps/dav/lib/Upload/FutureFile.php index 0b158e364cf..ba37c56978d 100644 --- a/apps/dav/lib/Upload/FutureFile.php +++ b/apps/dav/lib/Upload/FutureFile.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @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\Upload; @@ -36,18 +20,14 @@ use Sabre\DAV\IFile; * @package OCA\DAV\Upload */ class FutureFile implements \Sabre\DAV\IFile { - /** @var Directory */ - private $root; - /** @var string */ - private $name; - /** * @param Directory $root * @param string $name */ - public function __construct(Directory $root, $name) { - $this->root = $root; - $this->name = $name; + public function __construct( + private Directory $root, + private $name, + ) { } /** diff --git a/apps/dav/lib/Upload/PartFile.php b/apps/dav/lib/Upload/PartFile.php index 8bfe992a987..11900997a90 100644 --- a/apps/dav/lib/Upload/PartFile.php +++ b/apps/dav/lib/Upload/PartFile.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @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: 2021-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\DAV\Upload; @@ -32,14 +16,10 @@ use Sabre\DAV\IFile; * but handled directly by external storage services like S3 with Multipart Upload */ class PartFile implements IFile { - /** @var Directory */ - private $root; - /** @var array */ - private $partInfo; - - public function __construct(Directory $root, array $partInfo) { - $this->root = $root; - $this->partInfo = $partInfo; + public function __construct( + private Directory $root, + private array $partInfo, + ) { } /** diff --git a/apps/dav/lib/Upload/RootCollection.php b/apps/dav/lib/Upload/RootCollection.php index e05c154c8ea..cd7ab7f5e0a 100644 --- a/apps/dav/lib/Upload/RootCollection.php +++ b/apps/dav/lib/Upload/RootCollection.php @@ -3,49 +3,42 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Upload; +use OCP\Files\IRootFolder; +use OCP\IUserSession; +use OCP\Share\IManager; use Sabre\DAVACL\AbstractPrincipalCollection; use Sabre\DAVACL\PrincipalBackend; class RootCollection extends AbstractPrincipalCollection { - /** @var CleanupService */ - private $cleanupService; - - public function __construct(PrincipalBackend\BackendInterface $principalBackend, + public function __construct( + PrincipalBackend\BackendInterface $principalBackend, string $principalPrefix, - CleanupService $cleanupService) { + private CleanupService $cleanupService, + private IRootFolder $rootFolder, + private IUserSession $userSession, + private IManager $shareManager, + ) { parent::__construct($principalBackend, $principalPrefix); - $this->cleanupService = $cleanupService; } /** * @inheritdoc */ public function getChildForPrincipal(array $principalInfo): UploadHome { - return new UploadHome($principalInfo, $this->cleanupService); + return new UploadHome( + $principalInfo, + $this->cleanupService, + $this->rootFolder, + $this->userSession, + $this->shareManager, + ); } /** diff --git a/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php new file mode 100644 index 00000000000..a7030ba1133 --- /dev/null +++ b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Upload; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use function Sabre\Uri\split as uriSplit; + +/** + * Class that allows automatically creating non-existing collections on file + * upload. + * + * Since this functionality is not WebDAV compliant, it needs a special + * header to be activated. + */ +class UploadAutoMkcolPlugin extends ServerPlugin { + + private Server $server; + + public function initialize(Server $server): void { + $server->on('beforeMethod:PUT', [$this, 'beforeMethod']); + $this->server = $server; + } + + /** + * @throws NotFound a node expected to exist cannot be found + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response): bool { + if ($request->getHeader('X-NC-WebDAV-Auto-Mkcol') !== '1') { + return true; + } + + [$path,] = uriSplit($request->getPath()); + + if ($this->server->tree->nodeExists($path)) { + return true; + } + + $parts = explode('/', trim($path, '/')); + $rootPath = array_shift($parts); + $node = $this->server->tree->getNodeForPath('/' . $rootPath); + + if (!($node instanceof ICollection)) { + // the root node is not a collection, let SabreDAV handle it + return true; + } + + foreach ($parts as $part) { + if (!$node->childExists($part)) { + $node->createDirectory($part); + } + + $node = $node->getChild($part); + } + + return true; + } +} diff --git a/apps/dav/lib/Upload/UploadFile.php b/apps/dav/lib/Upload/UploadFile.php index efe1385c8ce..7301e855cfe 100644 --- a/apps/dav/lib/Upload/UploadFile.php +++ b/apps/dav/lib/Upload/UploadFile.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\Upload; @@ -29,11 +12,9 @@ use OCA\DAV\Connector\Sabre\File; use Sabre\DAV\IFile; class UploadFile implements IFile { - /** @var File */ - private $file; - - public function __construct(File $file) { - $this->file = $file; + public function __construct( + private File $file, + ) { } public function put($data) { diff --git a/apps/dav/lib/Upload/UploadFolder.php b/apps/dav/lib/Upload/UploadFolder.php index a1dade0e865..8890d472f87 100644 --- a/apps/dav/lib/Upload/UploadFolder.php +++ b/apps/dav/lib/Upload/UploadFolder.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Upload; @@ -28,21 +11,18 @@ use OC\Files\ObjectStore\ObjectStoreStorage; use OCA\DAV\Connector\Sabre\Directory; use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload; use OCP\Files\Storage\IStorage; +use OCP\ICacheFactory; +use OCP\Server; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; class UploadFolder implements ICollection { - /** @var Directory */ - private $node; - /** @var CleanupService */ - private $cleanupService; - /** @var IStorage */ - private $storage; - - public function __construct(Directory $node, CleanupService $cleanupService, IStorage $storage) { - $this->node = $node; - $this->cleanupService = $cleanupService; - $this->storage = $storage; + public function __construct( + private Directory $node, + private CleanupService $cleanupService, + private IStorage $storage, + private string $uid, + ) { } public function createFile($name, $data = null) { @@ -83,7 +63,7 @@ class UploadFolder implements ICollection { /** @var ObjectStoreStorage $storage */ $objectStore = $this->storage->getObjectStore(); if ($objectStore instanceof IObjectStoreMultiPartUpload) { - $cache = \OC::$server->getMemCacheFactory()->createDistributed(ChunkingV2Plugin::CACHE_KEY); + $cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY); $uploadSession = $cache->get($this->getName()); if ($uploadSession) { $uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID]; @@ -110,7 +90,7 @@ class UploadFolder implements ICollection { $this->node->delete(); // Background cleanup job is not needed anymore - $this->cleanupService->removeJob($this->getName()); + $this->cleanupService->removeJob($this->uid, $this->getName()); } public function getName() { diff --git a/apps/dav/lib/Upload/UploadHome.php b/apps/dav/lib/Upload/UploadHome.php index 6664d8c85b6..4042f1c4101 100644 --- a/apps/dav/lib/Upload/UploadHome.php +++ b/apps/dav/lib/Upload/UploadHome.php @@ -1,45 +1,43 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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\Upload; -use OC\Files\Filesystem; use OC\Files\View; use OCA\DAV\Connector\Sabre\Directory; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IUserSession; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; class UploadHome implements ICollection { - /** @var array */ - private $principalInfo; - /** @var CleanupService */ - private $cleanupService; - - public function __construct(array $principalInfo, CleanupService $cleanupService) { - $this->principalInfo = $principalInfo; - $this->cleanupService = $cleanupService; + private string $uid; + private ?Folder $uploadFolder = null; + + public function __construct( + private readonly array $principalInfo, + private readonly CleanupService $cleanupService, + private readonly IRootFolder $rootFolder, + private readonly IUserSession $userSession, + private readonly \OCP\Share\IManager $shareManager, + ) { + [$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']); + if ($prefix === 'principals/shares') { + $this->uid = $this->shareManager->getShareByToken($name)->getShareOwner(); + } else { + $user = $this->userSession->getUser(); + if (!$user) { + throw new Forbidden('Not logged in'); + } + + $this->uid = $user->getUID(); + } } public function createFile($name, $data = null) { @@ -50,16 +48,26 @@ class UploadHome implements ICollection { $this->impl()->createDirectory($name); // Add a cleanup job - $this->cleanupService->addJob($name); + $this->cleanupService->addJob($this->uid, $name); } public function getChild($name): UploadFolder { - return new UploadFolder($this->impl()->getChild($name), $this->cleanupService, $this->getStorage()); + return new UploadFolder( + $this->impl()->getChild($name), + $this->cleanupService, + $this->getStorage(), + $this->uid, + ); } public function getChildren(): array { return array_map(function ($node) { - return new UploadFolder($node, $this->cleanupService, $this->getStorage()); + return new UploadFolder( + $node, + $this->cleanupService, + $this->getStorage(), + $this->uid, + ); }, $this->impl()->getChildren()); } @@ -84,28 +92,29 @@ class UploadHome implements ICollection { return $this->impl()->getLastModified(); } - /** - * @return Directory - */ - private function impl() { - $view = $this->getView(); - $rootInfo = $view->getFileInfo(''); - return new Directory($view, $rootInfo); + private function getUploadFolder(): Folder { + if ($this->uploadFolder === null) { + $path = '/' . $this->uid . '/uploads'; + try { + $folder = $this->rootFolder->get($path); + if (!$folder instanceof Folder) { + throw new \Exception('Upload folder is a file'); + } + $this->uploadFolder = $folder; + } catch (NotFoundException $e) { + $this->uploadFolder = $this->rootFolder->newFolder($path); + } + } + return $this->uploadFolder; } - private function getView() { - $rootView = new View(); - $user = \OC::$server->getUserSession()->getUser(); - Filesystem::initMountPoints($user->getUID()); - if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) { - $rootView->mkdir('/' . $user->getUID() . '/uploads'); - } - return new View('/' . $user->getUID() . '/uploads'); + private function impl(): Directory { + $folder = $this->getUploadFolder(); + $view = new View($folder->getPath()); + return new Directory($view, $folder); } private function getStorage() { - $view = $this->getView(); - $storage = $view->getFileInfo('')->getStorage(); - return $storage; + return $this->getUploadFolder()->getStorage(); } } diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php index 22a20703024..73e9c375490 100644 --- a/apps/dav/lib/UserMigration/CalendarMigrator.php +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; @@ -58,17 +41,6 @@ class CalendarMigrator implements IMigrator, ISizeEstimationMigrator { use TMigratorBasicVersionHandling; - private CalDavBackend $calDavBackend; - - private ICalendarManager $calendarManager; - - // ICSExportPlugin is injected as the mergeObjects() method is required and is not to be used as a SabreDAV server plugin - private ICSExportPlugin $icsExportPlugin; - - private Defaults $defaults; - - private IL10N $l10n; - private SabreDavServer $sabreDavServer; private const USERS_URI_ROOT = 'principals/users/'; @@ -80,18 +52,12 @@ class CalendarMigrator implements IMigrator, ISizeEstimationMigrator { private const EXPORT_ROOT = Application::APP_ID . '/calendars/'; public function __construct( - CalDavBackend $calDavBackend, - ICalendarManager $calendarManager, - ICSExportPlugin $icsExportPlugin, - Defaults $defaults, - IL10N $l10n + private CalDavBackend $calDavBackend, + private ICalendarManager $calendarManager, + private ICSExportPlugin $icsExportPlugin, + private Defaults $defaults, + private IL10N $l10n, ) { - $this->calDavBackend = $calDavBackend; - $this->calendarManager = $calendarManager; - $this->icsExportPlugin = $icsExportPlugin; - $this->defaults = $defaults; - $this->l10n = $l10n; - $root = new RootCollection(); $this->sabreDavServer = new SabreDavServer(new CachingTree($root)); $this->sabreDavServer->addPlugin(new CalDAVPlugin()); diff --git a/apps/dav/lib/UserMigration/CalendarMigratorException.php b/apps/dav/lib/UserMigration/CalendarMigratorException.php index 3b4f8f89232..f3754809b44 100644 --- a/apps/dav/lib/UserMigration/CalendarMigratorException.php +++ b/apps/dav/lib/UserMigration/CalendarMigratorException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; diff --git a/apps/dav/lib/UserMigration/ContactsMigrator.php b/apps/dav/lib/UserMigration/ContactsMigrator.php index 38c53a1f76e..96d623938a3 100644 --- a/apps/dav/lib/UserMigration/ContactsMigrator.php +++ b/apps/dav/lib/UserMigration/ContactsMigrator.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; @@ -54,10 +37,6 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator { use TMigratorBasicVersionHandling; - private CardDavBackend $cardDavBackend; - - private IL10N $l10n; - private SabreDavServer $sabreDavServer; private const USERS_URI_ROOT = 'principals/users/'; @@ -71,12 +50,9 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator { private const PATH_ROOT = Application::APP_ID . '/address_books/'; public function __construct( - CardDavBackend $cardDavBackend, - IL10N $l10n + private CardDavBackend $cardDavBackend, + private IL10N $l10n, ) { - $this->cardDavBackend = $cardDavBackend; - $this->l10n = $l10n; - $root = new RootCollection(); $this->sabreDavServer = new SabreDavServer(new CachingTree($root)); $this->sabreDavServer->addPlugin(new CardDAVPlugin()); @@ -268,7 +244,7 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator { $vCard->serialize(), ); } catch (Throwable $e) { - $output->writeln("Error creating contact \"" . ($vCard->FN ?? 'null') . "\" from \"$filename\", skipping…"); + $output->writeln('Error creating contact "' . ($vCard->FN ?? 'null') . "\" from \"$filename\", skipping…"); } } diff --git a/apps/dav/lib/UserMigration/ContactsMigratorException.php b/apps/dav/lib/UserMigration/ContactsMigratorException.php index 63dedebf73d..8d64d3d4428 100644 --- a/apps/dav/lib/UserMigration/ContactsMigratorException.php +++ b/apps/dav/lib/UserMigration/ContactsMigratorException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; diff --git a/apps/dav/lib/UserMigration/InvalidAddressBookException.php b/apps/dav/lib/UserMigration/InvalidAddressBookException.php index fd99eac1a73..d904dd9d4dd 100644 --- a/apps/dav/lib/UserMigration/InvalidAddressBookException.php +++ b/apps/dav/lib/UserMigration/InvalidAddressBookException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; diff --git a/apps/dav/lib/UserMigration/InvalidCalendarException.php b/apps/dav/lib/UserMigration/InvalidCalendarException.php index 0e42ef1bc20..664989de8b1 100644 --- a/apps/dav/lib/UserMigration/InvalidCalendarException.php +++ b/apps/dav/lib/UserMigration/InvalidCalendarException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2022 Christopher Ng <chrng8@gmail.com> - * - * @author Christopher Ng <chrng8@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\UserMigration; |