diff options
Diffstat (limited to 'apps/dav/lib')
123 files changed, 1129 insertions, 407 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index edf7dd1214f..9d5921a1e83 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -20,7 +20,6 @@ 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; @@ -82,7 +81,6 @@ use OCP\Config\BeforePreferenceSetEvent; use OCP\Contacts\IManager as IContactsManager; use OCP\DB\Events\AddMissingIndicesEvent; use OCP\Federation\Events\TrustedServerRemovedEvent; -use OCP\Files\AppData\IAppDataFactory; use OCP\IUserSession; use OCP\Server; use OCP\Settings\Events\DeclarativeSettingsGetValueEvent; @@ -112,12 +110,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), diff --git a/apps/dav/lib/Avatars/RootCollection.php b/apps/dav/lib/Avatars/RootCollection.php index ec88c65793f..033dcaf7a5c 100644 --- a/apps/dav/lib/Avatars/RootCollection.php +++ b/apps/dav/lib/Avatars/RootCollection.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php index 7cc56698a9f..027b3349802 100644 --- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -40,8 +40,9 @@ class UserStatusAutomation extends TimedJob { ) { 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); } diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 137bad34bf5..d4faf3764e1 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php index 96a90f82cde..50f8cff76ba 100644 --- a/apps/dav/lib/BulkUpload/MultipartRequestParser.php +++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only @@ -57,7 +58,13 @@ 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); diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php index 5ae71f4ecc6..f0c49e6e28c 100644 --- a/apps/dav/lib/CalDAV/Activity/Backend.php +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php index 7411202044d..78579ee84b7 100644 --- a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php index 6bc7bd4b308..b001f90c28d 100644 --- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php index 9a75acb878c..558abe0ca1a 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -40,8 +41,8 @@ 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' => (string)$data['id'], diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php index 898eb2b5cb6..8c93ddae431 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php index 41b610542df..87551d7840b 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -78,14 +79,9 @@ class Event extends Base { // 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']); } - $link = [ - 'view' => 'dayGridMonth', - 'timeRange' => 'now', - 'mode' => 'sidebar', + $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 } diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php index 1e817663439..fc0625ec970 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php index a201213f784..0ad86a919bc 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Event.php b/apps/dav/lib/CalDAV/Activity/Setting/Event.php index ea049738251..ea9476d6f08 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Event.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php index 7ac3b1e0f76..ed8377b0ffa 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php index cd0b58c53d2..681709cdb6f 100644 --- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php +++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php index e1e46316d74..680b228766f 100644 --- a/apps/dav/lib/CalDAV/BirthdayService.php +++ b/apps/dav/lib/CalDAV/BirthdayService.php @@ -295,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/CachedSubscriptionImpl.php b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php index 74efebb6e2a..cc1bab6d4fc 100644 --- a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php +++ b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php @@ -55,16 +55,6 @@ class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendar return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color']; } - /** - * @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); } diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 5643e89d797..27750913105 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -212,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('*')) @@ -411,8 +410,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription // 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; } @@ -1040,7 +1039,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $rs->closeCursor(); } } - + /** * Returns all calendar objects with limited metadata for a calendar * @@ -1538,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. * @@ -2030,8 +2010,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription if ($pattern !== '') { $innerQuery->andWhere($innerQuery->expr()->iLike('op.value', - $outerQuery->createNamedParameter('%' . - $this->db->escapeLikeParameter($pattern) . '%'))); + $outerQuery->createNamedParameter('%' + . $this->db->escapeLikeParameter($pattern) . '%'))); } $start = null; @@ -3007,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); diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index 1d88d04a5e3..dd3a4cf3f69 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -53,8 +53,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable 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->l10n = $l10n; diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index d36f46df901..b79bf7ea2d0 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -93,16 +93,6 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs 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); diff --git a/apps/dav/lib/CalDAV/CalendarManager.php b/apps/dav/lib/CalDAV/CalendarManager.php index 1baf53ee457..a2d2f1cda8a 100644 --- a/apps/dav/lib/CalDAV/CalendarManager.php +++ b/apps/dav/lib/CalDAV/CalendarManager.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php index 90a1e97ec4d..02178b4236f 100644 --- a/apps/dav/lib/CalDAV/CalendarObject.php +++ b/apps/dav/lib/CalDAV/CalendarObject.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php index bfe5f84ce31..c0a313955bb 100644 --- a/apps/dav/lib/CalDAV/CalendarRoot.php +++ b/apps/dav/lib/CalDAV/CalendarRoot.php @@ -33,8 +33,8 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot { } 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]; diff --git a/apps/dav/lib/CalDAV/EventReader.php b/apps/dav/lib/CalDAV/EventReader.php index 7e337f3108a..b7dd2889956 100644 --- a/apps/dav/lib/CalDAV/EventReader.php +++ b/apps/dav/lib/CalDAV/EventReader.php @@ -169,9 +169,9 @@ class EventReader { 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(); + $this->baseEventDuration + = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() + - $this->baseEventStartDate->getTimeStamp(); } // evaluate if duration exists // extract duration and calculate end date @@ -362,8 +362,8 @@ class EventReader { public function recurringConcludes(): bool { // retrieve rrule conclusions - if ($this->rruleIterator?->concludesOn() !== null || - $this->rruleIterator?->concludesAfter() !== null) { + if ($this->rruleIterator?->concludesOn() !== null + || $this->rruleIterator?->concludesAfter() !== null) { return true; } // retrieve rdate conclusions diff --git a/apps/dav/lib/CalDAV/Export/ExportService.php b/apps/dav/lib/CalDAV/Export/ExportService.php index 393c53b92e4..552b9e2b675 100644 --- a/apps/dav/lib/CalDAV/Export/ExportService.php +++ b/apps/dav/lib/CalDAV/Export/ExportService.php @@ -18,7 +18,7 @@ use Sabre\VObject\Writer; * Calendar Export Service */ class ExportService { - + public const FORMATS = ['ical', 'jcal', 'xcal']; private string $systemVersion; @@ -92,7 +92,7 @@ class ExportService { default => Writer::write($vobject) }; } - + /** * Generates serialized content for a component in xml format */ diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php index 3f71b1db24c..08dc10f7bf4 100644 --- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php +++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php index 6e755716397..acf81638679 100644 --- a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php +++ b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php index bbee4cbda7c..40a8860dcb4 100644 --- a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php +++ b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php index 3d650a4a059..c8a7109abde 100644 --- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php +++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Outbox.php b/apps/dav/lib/CalDAV/Outbox.php index fc9dc87a574..608114d8093 100644 --- a/apps/dav/lib/CalDAV/Outbox.php +++ b/apps/dav/lib/CalDAV/Outbox.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Principal/Collection.php b/apps/dav/lib/CalDAV/Principal/Collection.php index f2cea0b5136..b76fde66464 100644 --- a/apps/dav/lib/CalDAV/Principal/Collection.php +++ b/apps/dav/lib/CalDAV/Principal/Collection.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Principal/User.php b/apps/dav/lib/CalDAV/Principal/User.php index 60b7953ea62..047d83827ed 100644 --- a/apps/dav/lib/CalDAV/Principal/User.php +++ b/apps/dav/lib/CalDAV/Principal/User.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php index 4ee811efeae..9af6e544165 100644 --- a/apps/dav/lib/CalDAV/PublicCalendar.php +++ b/apps/dav/lib/CalDAV/PublicCalendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/PublicCalendarObject.php b/apps/dav/lib/CalDAV/PublicCalendarObject.php index c3dc5ab1843..2ab40b94347 100644 --- a/apps/dav/lib/CalDAV/PublicCalendarObject.php +++ b/apps/dav/lib/CalDAV/PublicCalendarObject.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php index db73a761b0d..76378e7a1c5 100644 --- a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php +++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php index 2fd55a12643..fb9b7298f9b 100644 --- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php +++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index e2d44535ce0..c75090e1560 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -441,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; } diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php index 236039e4890..68bb3373346 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -85,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)) { @@ -470,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 40396f67ce9..c70d93daf52 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php index 91cf78c296f..5704b23ae14 100644 --- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php +++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 7e79388c53a..1f063540df6 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -45,7 +45,7 @@ use Sabre\VObject\Reader; * @license http://sabre.io/license/ Modified BSD License */ class IMipPlugin extends SabreIMipPlugin { - + private ?VCalendar $vCalendar = null; public const MAX_DATE = '2038-01-01'; public const METHOD_REQUEST = 'request'; @@ -156,9 +156,10 @@ class IMipPlugin extends SabreIMipPlugin { $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'; @@ -185,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: diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index e2844960a23..f7054eb2d34 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -159,7 +159,35 @@ class IMipService { if ($eventReaderCurrent->recurs()) { $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent); } - + return $data; + } + + /** + * @param VEvent $vEvent + * @return array + */ + 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); + } + + if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) { + $data['meeting_location_html'] = $locationHtml; + } + + $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : ''; + + // generate occurring next string + if ($eventReader->recurs()) { + $data['meeting_occurring'] = $this->generateOccurringString($eventReader); + } + return $data; } @@ -324,7 +352,7 @@ class IMipService { * @return string */ public function generateWhenStringRecurringDaily(EventReader $er): string { - + // initialize $interval = (int)$er->recurringInterval(); $startTime = null; @@ -375,7 +403,7 @@ class IMipService { * @return string */ public function generateWhenStringRecurringWeekly(EventReader $er): string { - + // initialize $interval = (int)$er->recurringInterval(); $startTime = null; @@ -428,15 +456,15 @@ class IMipService { * @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())); + $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()); } @@ -493,7 +521,7 @@ class IMipService { * @return string */ public function generateWhenStringRecurringYearly(EventReader $er): string { - + // initialize $interval = (int)$er->recurringInterval(); $startTime = null; @@ -502,8 +530,8 @@ class IMipService { $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())); + $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 = $er->startDateTime()->format('jS'); } @@ -582,7 +610,7 @@ class IMipService { 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 * @@ -1080,8 +1108,8 @@ class IMipService { $attendee = $iTipMessage->recipient; $organizer = $iTipMessage->sender; $sequence = $iTipMessage->sequence; - $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? - $vevent->{'RECURRENCE-ID'}->serialize() : null; + $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) + ? $vevent->{'RECURRENCE-ID'}->serialize() : null; $uid = $vevent->{'UID'}; $query = $this->db->getQueryBuilder(); @@ -1155,6 +1183,21 @@ class IMipService { 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) { diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index da0f9ea5637..a001df8b2a8 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -372,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); diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php index 602ce151e12..27e39a76305 100644 --- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php +++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -61,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; } /** diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php index 8a130865842..21a4fff1caf 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php index 943e657903e..a98b325397b 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php index 439a795dde9..ef438aa0258 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php index 3b03b63e909..0c31f32348a 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php index 42ecf630f44..251120e35cc 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php index b10cf3140cf..6d6bf958496 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php index 639d0b32655..6ece88fa87b 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php index 43eff124f0b..16e68fde1f0 100644 --- a/apps/dav/lib/CalDAV/TipBroker.php +++ b/apps/dav/lib/CalDAV/TipBroker.php @@ -97,7 +97,7 @@ class TipBroker extends Broker { // 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 @@ -108,7 +108,7 @@ class TipBroker extends Broker { $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'; @@ -124,11 +124,11 @@ class TipBroker extends Broker { $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']; + $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]; diff --git a/apps/dav/lib/CalDAV/UpcomingEventsService.php b/apps/dav/lib/CalDAV/UpcomingEventsService.php index 9c054deb4e0..6614d937ff7 100644 --- a/apps/dav/lib/CalDAV/UpcomingEventsService.php +++ b/apps/dav/lib/CalDAV/UpcomingEventsService.php @@ -47,20 +47,36 @@ class UpcomingEventsService { $this->userManager->get($userId), ); - return array_map(fn (array $event) => 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, - match ($calendarAppEnabled) { - // TODO: create a named, deep route in calendar - // TODO: it's a code smell to just assume this route exists, find an abstraction - true => $this->urlGenerator->linkToRouteAbsolute('calendar.view.index'), - false => null, - }, - ), $events); + return 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); + } + + 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/Capabilities.php b/apps/dav/lib/Capabilities.php index f321222b285..f9bad25bf31 100644 --- a/apps/dav/lib/Capabilities.php +++ b/apps/dav/lib/Capabilities.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 ownCloud GmbH * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/CardDAV/Activity/Filter.php b/apps/dav/lib/CardDAV/Activity/Filter.php index 8934c455def..8b221a29ff0 100644 --- a/apps/dav/lib/CardDAV/Activity/Filter.php +++ b/apps/dav/lib/CardDAV/Activity/Filter.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Base.php b/apps/dav/lib/CardDAV/Activity/Provider/Base.php index 0c73c8558d0..ea7680aed60 100644 --- a/apps/dav/lib/CardDAV/Activity/Provider/Base.php +++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php @@ -41,8 +41,8 @@ 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' => (string)$data['id'], diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index 2ec645f04d2..67c0b7167fa 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -38,8 +38,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'); } } diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index 8657460d0c6..6bb8e24f628 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -307,8 +307,8 @@ class AddressBookImpl implements IAddressBookEnabled { */ 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() ); } @@ -324,7 +324,7 @@ class AddressBookImpl implements IAddressBookEnabled { $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) { diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index de209754ae4..06f6bf9448e 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -127,7 +127,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; @@ -160,8 +159,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; } diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php index 2f1999b6b1c..03c71f7e4a3 100644 --- a/apps/dav/lib/CardDAV/PhotoCache.php +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -1,10 +1,13 @@ <?php + /** * 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; @@ -19,6 +22,7 @@ use Sabre\VObject\Property\Binary; use Sabre\VObject\Reader; class PhotoCache { + private ?IAppData $photoCacheAppData = null; /** @var array */ public const ALLOWED_CONTENT_TYPES = [ @@ -30,12 +34,9 @@ class PhotoCache { 'image/avif' => 'avif', ]; - /** - * PhotoCache constructor. - */ public function __construct( - protected IAppData $appData, - protected LoggerInterface $logger, + private IAppDataFactory $appDataFactory, + private LoggerInterface $logger, ) { } @@ -142,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; } } @@ -265,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/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/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php index 5758cd4fa87..6ed8aa2cfe4 100644 --- a/apps/dav/lib/Command/ExportCalendar.php +++ b/apps/dav/lib/Command/ExportCalendar.php @@ -79,7 +79,7 @@ class ExportCalendar extends Command { 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); } diff --git a/apps/dav/lib/Command/ListCalendars.php b/apps/dav/lib/Command/ListCalendars.php index 06a1f7397c4..408a7e5247f 100644 --- a/apps/dav/lib/Command/ListCalendars.php +++ b/apps/dav/lib/Command/ListCalendars.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php index 36f20e5b547..b8acc191cc3 100644 --- a/apps/dav/lib/Command/MoveCalendar.php +++ b/apps/dav/lib/Command/MoveCalendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php index f5afb30ed70..89bb5ce8c20 100644 --- a/apps/dav/lib/Command/SendEventReminders.php +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php index b39dc7197b0..0e2b1c58748 100644 --- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php index 88fff5e6a5a..9cff113140a 100644 --- a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php +++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php index d977721bdfa..a174920946a 100644 --- a/apps/dav/lib/Connector/Sabre/Auth.php +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -55,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; } /** @@ -71,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; @@ -118,7 +118,7 @@ class Auth extends AbstractBasic { * Checks whether a CSRF check is required on the request */ private function requiresCSRFCheck(): bool { - + $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS']; if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) { return false; @@ -144,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; } @@ -159,8 +159,8 @@ 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; @@ -178,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() && empty($request->getHeader('Authorization'))) || - \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; diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php index e189d8fa128..23453ae8efb 100644 --- a/apps/dav/lib/Connector/Sabre/BearerAuth.php +++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/CachingTree.php b/apps/dav/lib/Connector/Sabre/CachingTree.php index 86e102677c1..5d72b530f58 100644 --- a/apps/dav/lib/Connector/Sabre/CachingTree.php +++ b/apps/dav/lib/Connector/Sabre/CachingTree.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php index 7846896182f..100d719ef01 100644 --- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php +++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php @@ -62,7 +62,7 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin { ) ); } - + } return $access; diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php index 4a7e30caa10..f6baceb748b 100644 --- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php +++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php @@ -43,8 +43,8 @@ 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); diff --git a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php index 41ace002660..1e1e4aaed04 100644 --- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php +++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php index 61ecfaf845c..b0c5a079ce1 100644 --- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -117,8 +117,8 @@ class FakeLockerPlugin extends ServerPlugin { $lockInfo->timeout = 1800; $body = $this->server->xml->write('{DAV:}prop', [ - '{DAV:}lockdiscovery' => - new LockDiscovery([$lockInfo]) + '{DAV:}lockdiscovery' + => new LockDiscovery([$lockInfo]) ]); $response->setStatus(Http::STATUS_OK); diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 045b9d7e784..218d38e1c4b 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -19,6 +19,7 @@ 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; @@ -215,7 +216,9 @@ class File extends Node implements IFile { try { /** @var IWriteStreamStorage $partStorage */ $count = $partStorage->writeStream($internalPartPath, $wrappedData); - } catch (GenericFileException) { + } 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); @@ -229,7 +232,7 @@ class File extends Node implements IFile { // 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); } diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 9e2affddb6b..843383a0452 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -9,6 +9,7 @@ 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; @@ -256,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( [ @@ -374,7 +375,13 @@ class FilesPlugin extends ServerPlugin { } // Check if the user published their display name - $ownerAccount = $this->accountManager->getAccount($owner); + 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 @@ -534,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'; } diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php index efed6ce09f8..e18ef58149a 100644 --- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php +++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index e49c61f6e1f..b61cabedf5f 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php index b5d9ce3db72..2ca1c25e2f6 100644 --- a/apps/dav/lib/Connector/Sabre/PublicAuth.php +++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php @@ -14,6 +14,7 @@ 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; @@ -23,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; @@ -45,6 +47,7 @@ class PublicAuth extends AbstractBasic { private ISession $session, private IThrottler $throttler, private LoggerInterface $logger, + private IURLGenerator $urlGenerator, ) { // setup realm $defaults = new Defaults(); @@ -52,10 +55,6 @@ class PublicAuth extends AbstractBasic { } /** - * @param RequestInterface $request - * @param ResponseInterface $response - * - * @return array * @throws NotAuthenticated * @throws MaxDelayReached * @throws ServiceUnavailable @@ -64,6 +63,10 @@ class PublicAuth extends AbstractBasic { 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, @@ -80,6 +83,15 @@ class PublicAuth extends AbstractBasic { } 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); $msg = $e->getMessage(); @@ -90,7 +102,6 @@ class PublicAuth extends AbstractBasic { /** * Extract token from request url - * @return string * @throws NotFound */ private function getToken(): string { @@ -107,7 +118,7 @@ class PublicAuth extends AbstractBasic { /** * Check token validity - * @return array + * * @throws NotFound * @throws NotAuthenticated */ @@ -155,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 +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/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index 088cf33d85f..f49e85333f3 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -176,8 +176,8 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { 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/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php index eb06fa5cef6..25c1633df36 100644 --- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -94,6 +94,7 @@ 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']); } /** @@ -150,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 @@ -199,22 +218,11 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { )) { // 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; @@ -270,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/Controller/BirthdayCalendarController.php b/apps/dav/lib/Controller/BirthdayCalendarController.php index d3a9239dd22..f6bfb229a9c 100644 --- a/apps/dav/lib/Controller/BirthdayCalendarController.php +++ b/apps/dav/lib/Controller/BirthdayCalendarController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Controller/ExampleContentController.php b/apps/dav/lib/Controller/ExampleContentController.php index 9146eeb639d..e20ee4b7f49 100644 --- a/apps/dav/lib/Controller/ExampleContentController.php +++ b/apps/dav/lib/Controller/ExampleContentController.php @@ -10,77 +10,89 @@ declare(strict_types=1); namespace OCA\DAV\Controller; use OCA\DAV\AppInfo\Application; -use OCP\App\IAppManager; +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\Files\AppData\IAppDataFactory; -use OCP\Files\IAppData; -use OCP\Files\NotFoundException; -use OCP\IConfig; use OCP\IRequest; use Psr\Log\LoggerInterface; class ExampleContentController extends ApiController { - private IAppData $appData; public function __construct( IRequest $request, - private IConfig $config, - private IAppDataFactory $appDataFactory, - private IAppManager $appManager, - private LoggerInterface $logger, + private readonly LoggerInterface $logger, + private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, ) { parent::__construct(Application::APP_ID, $request); - $this->appData = $this->appDataFactory->get('dav'); } - public function setEnableDefaultContact($allow) { - if ($allow === 'yes' && !$this->defaultContactExists()) { + #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/config')] + public function setEnableDefaultContact(bool $allow): JSONResponse { + if ($allow && !$this->exampleContactService->defaultContactExists()) { try { - $this->setCard(); + $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->config->setAppValue(Application::APP_ID, 'enableDefaultContact', $allow); + $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->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'no')) { + if (!$this->exampleContactService->isDefaultContactEnabled()) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->setCard($contactData); + $this->exampleContactService->setCard($contactData); return new JSONResponse([], Http::STATUS_OK); } - private function setCard(?string $cardData = null) { - try { - $folder = $this->appData->getFolder('defaultContact'); - } catch (NotFoundException $e) { - $folder = $this->appData->newFolder('defaultContact'); - } + #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')] + public function setCreateExampleEvent(bool $enable): JSONResponse { + $this->exampleEventService->setCreateExampleEvent($enable); + return new JsonResponse([]); + } - if (is_null($cardData)) { - $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf'); - } + #[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', + ); + } - if (!$cardData) { - throw new \Exception('Could not read exampleContact.vcf'); + #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/event')] + public function uploadExampleEvent(string $ics): JSONResponse { + if (!$this->exampleEventService->shouldCreateExampleEvent()) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf'); - $file->putContent($cardData); + $this->exampleEventService->saveCustomExampleEvent($ics); + return new JsonResponse([]); } - private function defaultContactExists(): bool { - try { - $folder = $this->appData->getFolder('defaultContact'); - } catch (NotFoundException $e) { - return false; - } - return $folder->fileExists('defaultContact.vcf'); + #[FrontpageRoute(verb: 'DELETE', url: '/api/exampleEvent/event')] + public function deleteExampleEvent(): JSONResponse { + $this->exampleEventService->deleteCustomExampleEvent(); + return new JsonResponse([]); } } diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index c41ecd8450e..f3fff11b3da 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -92,6 +93,11 @@ class CustomPropertiesBackend implements BackendInterface { '{http://nextcloud.org/ns}lock-time', '{http://nextcloud.org/ns}lock-timeout', '{http://nextcloud.org/ns}lock-token', + // photos + '{http://nextcloud.org/ns}realpath', + '{http://nextcloud.org/ns}nbItems', + '{http://nextcloud.org/ns}face-detections', + '{http://nextcloud.org/ns}face-preview-image', ]; /** @@ -277,8 +283,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(); diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php index 143fc7d69f1..77ba45182c9 100644 --- a/apps/dav/lib/DAV/GroupPrincipalBackend.php +++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -50,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); + } } } @@ -77,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); } @@ -186,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/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index de0d6891b7c..d60f5cca7c6 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -64,8 +64,8 @@ abstract class Backend { } $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; } @@ -199,7 +199,7 @@ abstract class Backend { public function unshare(IShareable $shareable, string $principalUri): bool { $this->shareCache->clear(); - + $principal = $this->principalBackend->findByUri($principalUri, ''); if (empty($principal)) { return false; diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php index 4c3b49a45b0..9b9615b8063 100644 --- a/apps/dav/lib/DAV/ViewOnlyPlugin.php +++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php @@ -84,18 +84,25 @@ class ViewOnlyPlugin extends ServerPlugin { if (!$storage->instanceOfStorage(ISharedStorage::class)) { return true; } + // Extract extra permissions /** @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/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/UnsupportedLimitOnInitialSyncException.php b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php index 1022625c23b..c6b7f8564c5 100644 --- a/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php +++ b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index ace367e4490..eb548bbd55c 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -15,6 +16,7 @@ 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; @@ -44,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, @@ -133,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 { diff --git a/apps/dav/lib/Files/LazySearchBackend.php b/apps/dav/lib/Files/LazySearchBackend.php index a0ad730ff2b..6ba539ddd87 100644 --- a/apps/dav/lib/Files/LazySearchBackend.php +++ b/apps/dav/lib/Files/LazySearchBackend.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php index ad7648795da..a3dbd32ce6b 100644 --- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -1,12 +1,15 @@ <?php + /** * 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; @@ -17,14 +20,9 @@ use Sabre\HTTP\ResponseInterface; */ class FilesDropPlugin extends ServerPlugin { - private ?View $view = null; private ?IShare $share = null; private bool $enabled = false; - public function setView(View $view): void { - $this->view = $view; - } - public function setShare(IShare $share): void { $this->share = $share; } @@ -33,7 +31,6 @@ class FilesDropPlugin extends ServerPlugin { $this->enabled = true; } - /** * This initializes the plugin. * It is ONLY initialized by the server on a file drop request. @@ -45,7 +42,12 @@ class FilesDropPlugin extends ServerPlugin { } public function onMkcol(RequestInterface $request, ResponseInterface $response) { - if (!$this->enabled || $this->share === null || $this->view === null) { + if (!$this->enabled || $this->share === null) { + return; + } + + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { return; } @@ -57,7 +59,12 @@ class FilesDropPlugin extends ServerPlugin { } public function beforeMethod(RequestInterface $request, ResponseInterface $response) { - if (!$this->enabled || $this->share === null || $this->view === null) { + if (!$this->enabled || $this->share === null) { + return; + } + + $node = $this->share->getNode(); + if (!($node instanceof Folder)) { return; } @@ -66,13 +73,12 @@ class FilesDropPlugin extends ServerPlugin { ? trim(urldecode($request->getHeader('X-NC-Nickname'))) : null; - // if ($request->getMethod() !== 'PUT') { // If uploading subfolders we need to ensure they get created // within the nickname folder if ($request->getMethod() === 'MKCOL') { if (!$nickname) { - throw new MethodNotAllowed('A nickname header is required when uploading subfolders'); + throw new BadRequest('A nickname header is required when uploading subfolders'); } } else { throw new MethodNotAllowed('Only PUT is allowed on files drop'); @@ -108,7 +114,7 @@ class FilesDropPlugin extends ServerPlugin { // We need a valid nickname for file requests if ($isFileRequest && !$nickname) { - throw new MethodNotAllowed('A nickname header is required for file requests'); + throw new BadRequest('A nickname header is required for file requests'); } // We're only allowing the upload of @@ -116,56 +122,78 @@ class FilesDropPlugin extends ServerPlugin { // This prevents confusion when uploading files and help // classify them by uploaders. if (!$nickname && !$isRootUpload) { - throw new MethodNotAllowed('A nickname header is required when uploading subfolders'); + throw new BadRequest('A nickname header is required when uploading subfolders'); } - // If we have a nickname, let's put everything inside if ($nickname) { - // Put all files in the subfolder + try { + $node->verifyPath($nickname); + } catch (\Exception $e) { + // If the path is not valid, we throw an exception + throw new BadRequest('Invalid nickname: ' . $nickname); + } + + // 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 - $folders = $this->getPathSegments(dirname($relativePath)); - foreach ($folders as $folder) { - if ($folder === '') { + $folder = $node; + $pathSegments = $this->getPathSegments(dirname($relativePath)); + foreach ($pathSegments as $pathSegment) { + if ($pathSegment === '') { continue; - } // skip empty parts - if (!$this->view->file_exists($folder)) { - $this->view->mkdir($folder); + } + + 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 - $noConflictPath = \OC_Helper::buildNotExistingFileNameForView(dirname($relativePath), basename($relativePath), $this->view); - $path = '/files/' . $token . '/' . $noConflictPath; - $url = $request->getBaseUrl() . str_replace('//', '/', $path); + $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 = rtrim(str_replace('\\', '/', $path), '/'); - - // Handle absolute paths starting with / - $isAbsolute = str_starts_with($path, '/'); - - $segments = explode('/', $path); - - // Add back the leading slash for the first segment if needed - $result = []; - $current = $isAbsolute ? '/' : ''; - - foreach ($segments as $segment) { - if ($segment === '') { - // skip empty parts - continue; - } - $current = rtrim($current, '/') . '/' . $segment; - $result[] = $current; - } + $path = trim(str_replace('\\', '/', $path), '/'); - return $result; + return explode('/', $path); } } diff --git a/apps/dav/lib/Listener/DavAdminSettingsListener.php b/apps/dav/lib/Listener/DavAdminSettingsListener.php index c59c2df1575..69501915208 100644 --- a/apps/dav/lib/Listener/DavAdminSettingsListener.php +++ b/apps/dav/lib/Listener/DavAdminSettingsListener.php @@ -42,11 +42,11 @@ class DavAdminSettingsListener implements IEventListener { $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)); } diff --git a/apps/dav/lib/Listener/UserEventsListener.php b/apps/dav/lib/Listener/UserEventsListener.php index 61d945e829b..a6b09b70fa0 100644 --- a/apps/dav/lib/Listener/UserEventsListener.php +++ b/apps/dav/lib/Listener/UserEventsListener.php @@ -9,17 +9,19 @@ declare(strict_types=1); 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\DefaultContactService; +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\Server; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\BeforeUserIdUnassignedEvent; use OCP\User\Events\UserChangedEvent; @@ -46,7 +48,10 @@ class UserEventsListener implements IEventListener { private CalDavBackend $calDav, private CardDavBackend $cardDav, private Defaults $themingDefaults, - private DefaultContactService $defaultContactService, + private ExampleContactService $exampleContactService, + private ExampleEventService $exampleEventService, + private LoggerInterface $logger, + private IJobList $jobList, ) { } @@ -122,6 +127,8 @@ class UserEventsListener implements IEventListener { $this->cardDav->deleteAddressBook($addressBook['id']); } + $this->jobList->remove(UserStatusAutomation::class, ['userId' => $uid]); + unset($this->calendarsToDelete[$uid]); unset($this->subscriptionsToDelete[$uid]); unset($this->addressBooksToDelete[$uid]); @@ -137,17 +144,31 @@ class UserEventsListener implements IEventListener { public function firstLogin(IUser $user): void { $principal = 'principals/users/' . $user->getUID(); + + $calendarId = null; if ($this->calDav->getCalendarsForUserCount($principal) === 0) { try { - $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [ + $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) { - Server::get(LoggerInterface::class)->error($e->getMessage(), ['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 { @@ -155,11 +176,11 @@ class UserEventsListener implements IEventListener { '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME, ]); } catch (\Exception $e) { - Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + $this->logger->error($e->getMessage(), ['exception' => $e]); } } if ($addressBookId) { - $this->defaultContactService->createDefaultContact($addressBookId); + $this->exampleContactService->createDefaultContact($addressBookId); } } } diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php index b157934a1ff..d8f906f22ee 100644 --- a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndex.php b/apps/dav/lib/Migration/BuildSocialSearchIndex.php index 5fab3f4ef77..a808034365a 100644 --- a/apps/dav/lib/Migration/BuildSocialSearchIndex.php +++ b/apps/dav/lib/Migration/BuildSocialSearchIndex.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php index ecc462e153b..24e182e46eb 100644 --- a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php +++ b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php index 14037801eb4..ef8e9002e9d 100644 --- a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php +++ b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/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/RemoveObjectProperties.php b/apps/dav/lib/Migration/RemoveObjectProperties.php index 3f505ecb1e2..f09293ae0bb 100644 --- a/apps/dav/lib/Migration/RemoveObjectProperties.php +++ b/apps/dav/lib/Migration/RemoveObjectProperties.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/Migration/Version1004Date20170825134824.php b/apps/dav/lib/Migration/Version1004Date20170825134824.php index 54c4c194778..4bf9637b697 100644 --- a/apps/dav/lib/Migration/Version1004Date20170825134824.php +++ b/apps/dav/lib/Migration/Version1004Date20170825134824.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/Version1004Date20170919104507.php b/apps/dav/lib/Migration/Version1004Date20170919104507.php index ca20e2fb4e7..678d92d2b83 100644 --- a/apps/dav/lib/Migration/Version1004Date20170919104507.php +++ b/apps/dav/lib/Migration/Version1004Date20170919104507.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/Version1004Date20170924124212.php b/apps/dav/lib/Migration/Version1004Date20170924124212.php index fbfec7e8e2d..4d221e91132 100644 --- a/apps/dav/lib/Migration/Version1004Date20170924124212.php +++ b/apps/dav/lib/Migration/Version1004Date20170924124212.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/Version1004Date20170926103422.php b/apps/dav/lib/Migration/Version1004Date20170926103422.php index 38506b0fc5d..ec56e035006 100644 --- a/apps/dav/lib/Migration/Version1004Date20170926103422.php +++ b/apps/dav/lib/Migration/Version1004Date20170926103422.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/Version1005Date20180530124431.php b/apps/dav/lib/Migration/Version1005Date20180530124431.php index ac1994893fd..b5f9ff26962 100644 --- a/apps/dav/lib/Migration/Version1005Date20180530124431.php +++ b/apps/dav/lib/Migration/Version1005Date20180530124431.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Migration/Version1006Date20180619154313.php b/apps/dav/lib/Migration/Version1006Date20180619154313.php index 195209ed046..231861a68c4 100644 --- a/apps/dav/lib/Migration/Version1006Date20180619154313.php +++ b/apps/dav/lib/Migration/Version1006Date20180619154313.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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/PaginatePlugin.php b/apps/dav/lib/Paginate/PaginatePlugin.php index c02eb9f21eb..c5da18f5c47 100644 --- a/apps/dav/lib/Paginate/PaginatePlugin.php +++ b/apps/dav/lib/Paginate/PaginatePlugin.php @@ -49,8 +49,8 @@ class PaginatePlugin extends ServerPlugin { } $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))) + $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); @@ -68,9 +68,9 @@ class PaginatePlugin extends ServerPlugin { 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)) + $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); diff --git a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php index 198a09b4bc8..bb098a0f107 100644 --- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php +++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -57,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 be5831feee3..258138caa42 100644 --- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php +++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index f1595bab391..f81c7fa6f29 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -63,6 +64,7 @@ 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; @@ -72,7 +74,6 @@ use OCP\Comments\ICommentsManager; use OCP\Defaults; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\AppData\IAppDataFactory; use OCP\Files\IFilenameValidator; use OCP\Files\IRootFolder; use OCP\FilesMetadata\IFilesMetadataManager; @@ -216,10 +217,7 @@ class Server { $this->server->addPlugin(new VCFExportPlugin()); $this->server->addPlugin(new MultiGetExportPlugin()); $this->server->addPlugin(new HasPhotoPlugin()); - $this->server->addPlugin(new ImageExportPlugin(new PhotoCache( - \OCP\Server::get(IAppDataFactory::class)->get('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)); @@ -236,6 +234,7 @@ class Server { $this->server->addPlugin(new CopyEtagHeaderPlugin()); $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( @@ -356,6 +355,7 @@ class Server { \OCP\Server::get(IAppManager::class) )); $lazySearchBackend->setBackend(new FileSearchBackend( + $this->server, $this->server->tree, $user, \OCP\Server::get(IRootFolder::class), diff --git a/apps/dav/lib/Service/DefaultContactService.php b/apps/dav/lib/Service/DefaultContactService.php deleted file mode 100644 index ad7a1179195..00000000000 --- a/apps/dav/lib/Service/DefaultContactService.php +++ /dev/null @@ -1,77 +0,0 @@ -<?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\App\IAppManager; -use OCP\Files\AppData\IAppDataFactory; -use OCP\IAppConfig; -use Psr\Log\LoggerInterface; -use Symfony\Component\Uid\Uuid; - -class DefaultContactService { - public function __construct( - private CardDavBackend $cardDav, - private IAppManager $appManager, - private IAppDataFactory $appDataFactory, - private IAppConfig $config, - private LoggerInterface $logger, - ) { - } - - public function createDefaultContact(int $addressBookId): void { - $enableDefaultContact = $this->config->getValueString(Application::APP_ID, 'enableDefaultContact', 'no'); - if ($enableDefaultContact !== 'yes') { - return; - } - $appData = $this->appDataFactory->get('dav'); - try { - $folder = $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(), 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/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/CalDAVSettings.php b/apps/dav/lib/Settings/CalDAVSettings.php index 5b6b7fa7e3d..5e19539a899 100644 --- a/apps/dav/lib/Settings/CalDAVSettings.php +++ b/apps/dav/lib/Settings/CalDAVSettings.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Settings/ExampleContentSettings.php b/apps/dav/lib/Settings/ExampleContentSettings.php index f5607b6a31b..7b6f9b03a3a 100644 --- a/apps/dav/lib/Settings/ExampleContentSettings.php +++ b/apps/dav/lib/Settings/ExampleContentSettings.php @@ -9,28 +9,56 @@ declare(strict_types=1); 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\IConfig; use OCP\Settings\ISettings; class ExampleContentSettings implements ISettings { - public function __construct( - private IConfig $config, - private IInitialState $initialState, - private IAppManager $appManager, + private readonly IAppConfig $appConfig, + private readonly IInitialState $initialState, + private readonly IAppManager $appManager, + private readonly ExampleEventService $exampleEventService, + private readonly ExampleContactService $exampleContactService, ) { } public function getForm(): TemplateResponse { - $enableDefaultContact = $this->config->getAppValue(Application::APP_ID, 'enableDefaultContact', 'no'); - $this->initialState->provideInitialState('enableDefaultContact', $enableDefaultContact); + $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')) { + if (!$this->appManager->isEnabledForUser('contacts') + && !$this->appManager->isEnabledForUser('calendar')) { return null; } @@ -40,5 +68,4 @@ class ExampleContentSettings implements ISettings { public function getPriority(): int { return 10; } - } diff --git a/apps/dav/lib/SystemTag/SystemTagList.php b/apps/dav/lib/SystemTag/SystemTagList.php index 546467f562e..b55f10164d7 100644 --- a/apps/dav/lib/SystemTag/SystemTagList.php +++ b/apps/dav/lib/SystemTag/SystemTagList.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php index da51279a9d2..2341d4823ba 100644 --- a/apps/dav/lib/SystemTag/SystemTagNode.php +++ b/apps/dav/lib/SystemTag/SystemTagNode.php @@ -122,8 +122,8 @@ class SystemTagNode implements \Sabre\DAV\ICollection { 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' ); } } @@ -176,15 +176,15 @@ class SystemTagNode implements \Sabre\DAV\ICollection { 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); diff --git a/apps/dav/lib/SystemTag/SystemTagObjectType.php b/apps/dav/lib/SystemTag/SystemTagObjectType.php index 0e1368854cd..0d348cd95f4 100644 --- a/apps/dav/lib/SystemTag/SystemTagObjectType.php +++ b/apps/dav/lib/SystemTag/SystemTagObjectType.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectList.php b/apps/dav/lib/SystemTag/SystemTagsObjectList.php index 5ccc924eb53..64e8b1bbebb 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectList.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectList.php @@ -45,7 +45,7 @@ class SystemTagsObjectList implements XmlSerializable, XmlDeserializable { if ($tree === null) { return null; } - + $objects = []; foreach ($tree as $elem) { if ($elem['name'] === self::OBJECTID_ROOT_PROPERTYNAME) { diff --git a/apps/dav/lib/Traits/PrincipalProxyTrait.php b/apps/dav/lib/Traits/PrincipalProxyTrait.php index 279f796b720..feec485fe5c 100644 --- a/apps/dav/lib/Traits/PrincipalProxyTrait.php +++ b/apps/dav/lib/Traits/PrincipalProxyTrait.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/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; + } +} |