aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib')
-rw-r--r--apps/dav/lib/AppInfo/Application.php146
-rw-r--r--apps/dav/lib/AppInfo/PluginManager.php44
-rw-r--r--apps/dav/lib/Avatars/AvatarHome.php37
-rw-r--r--apps/dav/lib/Avatars/AvatarNode.php42
-rw-r--r--apps/dav/lib/Avatars/RootCollection.php29
-rw-r--r--apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php58
-rw-r--r--apps/dav/lib/BackgroundJob/CalendarRetentionJob.php31
-rw-r--r--apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php31
-rw-r--r--apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php31
-rw-r--r--apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php89
-rw-r--r--apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php35
-rw-r--r--apps/dav/lib/BackgroundJob/EventReminderJob.php47
-rw-r--r--apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php40
-rw-r--r--apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php23
-rw-r--r--apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php42
-rw-r--r--apps/dav/lib/BackgroundJob/RefreshWebcalJob.php31
-rw-r--r--apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php41
-rw-r--r--apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php438
-rw-r--r--apps/dav/lib/BackgroundJob/UploadCleanup.php44
-rw-r--r--apps/dav/lib/BackgroundJob/UserStatusAutomation.php50
-rw-r--r--apps/dav/lib/BulkUpload/BulkUploadPlugin.php45
-rw-r--r--apps/dav/lib/BulkUpload/MultipartRequestParser.php88
-rw-r--r--apps/dav/lib/CalDAV/Activity/Backend.php83
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Calendar.php40
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Todo.php39
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php53
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Calendar.php54
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Event.php112
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Todo.php43
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php30
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Calendar.php27
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Event.php27
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Todo.php27
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php29
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php35
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php34
-rw-r--r--apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php21
-rw-r--r--apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php21
-rw-r--r--apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php57
-rw-r--r--apps/dav/lib/CalDAV/BirthdayService.php79
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscription.php29
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionImpl.php102
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionObject.php21
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionProvider.php40
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php945
-rw-r--r--apps/dav/lib/CalDAV/Calendar.php72
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php61
-rw-r--r--apps/dav/lib/CalDAV/CalendarImpl.php146
-rw-r--r--apps/dav/lib/CalDAV/CalendarManager.php47
-rw-r--r--apps/dav/lib/CalDAV/CalendarObject.php62
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php83
-rw-r--r--apps/dav/lib/CalDAV/CalendarRoot.php46
-rw-r--r--apps/dav/lib/CalDAV/DefaultCalendarValidator.php41
-rw-r--r--apps/dav/lib/CalDAV/EmbeddedCalDavServer.php118
-rw-r--r--apps/dav/lib/CalDAV/EventComparisonService.php35
-rw-r--r--apps/dav/lib/CalDAV/EventReader.php771
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRDate.php35
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRRule.php87
-rw-r--r--apps/dav/lib/CalDAV/Export/ExportService.php107
-rw-r--r--apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php21
-rw-r--r--apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php32
-rw-r--r--apps/dav/lib/CalDAV/IRestorable.php21
-rw-r--r--apps/dav/lib/CalDAV/Integration/ExternalCalendar.php35
-rw-r--r--apps/dav/lib/CalDAV/Integration/ICalendarProvider.php22
-rw-r--r--apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php61
-rw-r--r--apps/dav/lib/CalDAV/Outbox.php32
-rw-r--r--apps/dav/lib/CalDAV/Plugin.php24
-rw-r--r--apps/dav/lib/CalDAV/Principal/Collection.php23
-rw-r--r--apps/dav/lib/CalDAV/Principal/User.php23
-rw-r--r--apps/dav/lib/CalDAV/Proxy/Proxy.php28
-rw-r--r--apps/dav/lib/CalDAV/Proxy/ProxyMapper.php23
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendar.php24
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarObject.php23
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarRoot.php50
-rw-r--r--apps/dav/lib/CalDAV/Publishing/PublishPlugin.php71
-rw-r--r--apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php40
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Backend.php75
-rw-r--r--apps/dav/lib/CalDAV/Reminder/INotificationProvider.php26
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php53
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php45
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php50
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php33
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Notifier.php54
-rw-r--r--apps/dav/lib/CalDAV/Reminder/ReminderService.php154
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php80
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php22
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php22
-rw-r--r--apps/dav/lib/CalDAV/RetentionService.php45
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php216
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php921
-rw-r--r--apps/dav/lib/CalDAV/Schedule/Plugin.php142
-rw-r--r--apps/dav/lib/CalDAV/Search/SearchPlugin.php35
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php25
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php25
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php24
-rw-r--r--apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php46
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Backend.php21
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Service.php22
-rw-r--r--apps/dav/lib/CalDAV/Status/StatusService.php80
-rw-r--r--apps/dav/lib/CalDAV/TimeZoneFactory.php213
-rw-r--r--apps/dav/lib/CalDAV/TimezoneService.php28
-rw-r--r--apps/dav/lib/CalDAV/TipBroker.php187
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php54
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php38
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/Plugin.php31
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php21
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php35
-rw-r--r--apps/dav/lib/CalDAV/UpcomingEvent.php69
-rw-r--r--apps/dav/lib/CalDAV/UpcomingEventsService.php86
-rw-r--r--apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php40
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/Connection.php143
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/Plugin.php48
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php290
-rw-r--r--apps/dav/lib/Capabilities.php40
-rw-r--r--apps/dav/lib/CardDAV/Activity/Backend.php75
-rw-r--r--apps/dav/lib/CardDAV/Activity/Filter.php39
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php48
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Base.php51
-rw-r--r--apps/dav/lib/CardDAV/Activity/Provider/Card.php54
-rw-r--r--apps/dav/lib/CardDAV/Activity/Setting.php25
-rw-r--r--apps/dav/lib/CardDAV/AddressBook.php42
-rw-r--r--apps/dav/lib/CardDAV/AddressBookImpl.php128
-rw-r--r--apps/dav/lib/CardDAV/AddressBookRoot.php45
-rw-r--r--apps/dav/lib/CardDAV/Card.php22
-rw-r--r--apps/dav/lib/CardDAV/CardDavBackend.php214
-rw-r--r--apps/dav/lib/CardDAV/ContactsManager.php61
-rw-r--r--apps/dav/lib/CardDAV/Converter.php68
-rw-r--r--apps/dav/lib/CardDAV/HasPhotoPlugin.php23
-rw-r--r--apps/dav/lib/CardDAV/ImageExportPlugin.php39
-rw-r--r--apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php32
-rw-r--r--apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php21
-rw-r--r--apps/dav/lib/CardDAV/MultiGetExportPlugin.php27
-rw-r--r--apps/dav/lib/CardDAV/PhotoCache.php70
-rw-r--r--apps/dav/lib/CardDAV/Plugin.php24
-rw-r--r--apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php86
-rw-r--r--apps/dav/lib/CardDAV/Sharing/Backend.php21
-rw-r--r--apps/dav/lib/CardDAV/Sharing/Service.php22
-rw-r--r--apps/dav/lib/CardDAV/SyncService.php279
-rw-r--r--apps/dav/lib/CardDAV/SystemAddressbook.php61
-rw-r--r--apps/dav/lib/CardDAV/UserAddressBooks.php54
-rw-r--r--apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php40
-rw-r--r--apps/dav/lib/CardDAV/Xml/Groups.php34
-rw-r--r--apps/dav/lib/Command/ClearCalendarUnshares.php114
-rw-r--r--apps/dav/lib/Command/ClearContactsPhotoCache.php75
-rw-r--r--apps/dav/lib/Command/CreateAddressBook.php41
-rw-r--r--apps/dav/lib/Command/CreateCalendar.php54
-rw-r--r--apps/dav/lib/Command/CreateSubscription.php78
-rw-r--r--apps/dav/lib/Command/DeleteCalendar.php26
-rw-r--r--apps/dav/lib/Command/DeleteSubscription.php79
-rw-r--r--apps/dav/lib/Command/ExportCalendar.php95
-rw-r--r--apps/dav/lib/Command/FixCalendarSyncCommand.php37
-rw-r--r--apps/dav/lib/Command/GetAbsenceCommand.php62
-rw-r--r--apps/dav/lib/Command/ListAddressbooks.php76
-rw-r--r--apps/dav/lib/Command/ListCalendarShares.php131
-rw-r--r--apps/dav/lib/Command/ListCalendars.php27
-rw-r--r--apps/dav/lib/Command/ListSubscriptions.php77
-rw-r--r--apps/dav/lib/Command/MoveCalendar.php36
-rw-r--r--apps/dav/lib/Command/RemoveInvalidShares.php28
-rw-r--r--apps/dav/lib/Command/RetentionCleanupCommand.php21
-rw-r--r--apps/dav/lib/Command/SendEventReminders.php23
-rw-r--r--apps/dav/lib/Command/SetAbsenceCommand.php95
-rw-r--r--apps/dav/lib/Command/SyncBirthdayCalendar.php29
-rw-r--r--apps/dav/lib/Command/SyncSystemAddressBook.php26
-rw-r--r--apps/dav/lib/Comments/CommentNode.php56
-rw-r--r--apps/dav/lib/Comments/CommentsPlugin.php53
-rw-r--r--apps/dav/lib/Comments/EntityCollection.php35
-rw-r--r--apps/dav/lib/Comments/EntityTypeCollection.php39
-rw-r--r--apps/dav/lib/Comments/RootCollection.php46
-rw-r--r--apps/dav/lib/Connector/LegacyDAVACL.php26
-rw-r--r--apps/dav/lib/Connector/LegacyPublicAuth.php52
-rw-r--r--apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php26
-rw-r--r--apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php26
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php109
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php59
-rw-r--r--apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php57
-rw-r--r--apps/dav/lib/Connector/Sabre/CachingTree.php25
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumList.php23
-rw-r--r--apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php38
-rw-r--r--apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php25
-rw-r--r--apps/dav/lib/Connector/Sabre/DavAclPlugin.php51
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php191
-rw-r--r--apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php34
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/BadGateway.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/FileLocked.php32
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/Forbidden.php35
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php36
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php24
-rw-r--r--apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php43
-rw-r--r--apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php42
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php374
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php270
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesReportPlugin.php122
-rw-r--r--apps/dav/lib/Connector/Sabre/LockPlugin.php31
-rw-r--r--apps/dav/lib/Connector/Sabre/MaintenancePlugin.php39
-rw-r--r--apps/dav/lib/Connector/Sabre/MtimeSanitizer.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php146
-rw-r--r--apps/dav/lib/Connector/Sabre/ObjectTree.php85
-rw-r--r--apps/dav/lib/Connector/Sabre/Principal.php115
-rw-r--r--apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php78
-rw-r--r--apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php21
-rw-r--r--apps/dav/lib/Connector/Sabre/PublicAuth.php100
-rw-r--r--apps/dav/lib/Connector/Sabre/QuotaPlugin.php95
-rw-r--r--apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php27
-rw-r--r--apps/dav/lib/Connector/Sabre/Server.php228
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php229
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareTypeList.php38
-rw-r--r--apps/dav/lib/Connector/Sabre/ShareeList.php32
-rw-r--r--apps/dav/lib/Connector/Sabre/SharesPlugin.php110
-rw-r--r--apps/dav/lib/Connector/Sabre/TagList.php39
-rw-r--r--apps/dav/lib/Connector/Sabre/TagsPlugin.php116
-rw-r--r--apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php193
-rw-r--r--apps/dav/lib/Controller/BirthdayCalendarController.php77
-rw-r--r--apps/dav/lib/Controller/DirectController.php76
-rw-r--r--apps/dav/lib/Controller/ExampleContentController.php98
-rw-r--r--apps/dav/lib/Controller/InvitationResponseController.php70
-rw-r--r--apps/dav/lib/Controller/OutOfOfficeController.php46
-rw-r--r--apps/dav/lib/Controller/UpcomingEventsController.php57
-rw-r--r--apps/dav/lib/DAV/CustomPropertiesBackend.php158
-rw-r--r--apps/dav/lib/DAV/GroupPrincipalBackend.php66
-rw-r--r--apps/dav/lib/DAV/PublicAuth.php27
-rw-r--r--apps/dav/lib/DAV/Sharing/Backend.php99
-rw-r--r--apps/dav/lib/DAV/Sharing/IShareable.php24
-rw-r--r--apps/dav/lib/DAV/Sharing/Plugin.php50
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingMapper.php72
-rw-r--r--apps/dav/lib/DAV/Sharing/SharingService.php36
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/Invite.php57
-rw-r--r--apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php36
-rw-r--r--apps/dav/lib/DAV/SystemPrincipalBackend.php23
-rw-r--r--apps/dav/lib/DAV/ViewOnlyPlugin.php46
-rw-r--r--apps/dav/lib/Db/Absence.php37
-rw-r--r--apps/dav/lib/Db/AbsenceMapper.php21
-rw-r--r--apps/dav/lib/Db/Direct.php30
-rw-r--r--apps/dav/lib/Db/DirectMapper.php23
-rw-r--r--apps/dav/lib/Db/Property.php22
-rw-r--r--apps/dav/lib/Db/PropertyMapper.php36
-rw-r--r--apps/dav/lib/Direct/DirectFile.php41
-rw-r--r--apps/dav/lib/Direct/DirectHome.php60
-rw-r--r--apps/dav/lib/Direct/Server.php21
-rw-r--r--apps/dav/lib/Direct/ServerFactory.php35
-rw-r--r--apps/dav/lib/Events/AddressBookCreatedEvent.php35
-rw-r--r--apps/dav/lib/Events/AddressBookDeletedEvent.php41
-rw-r--r--apps/dav/lib/Events/AddressBookShareUpdatedEvent.php53
-rw-r--r--apps/dav/lib/Events/AddressBookUpdatedEvent.php47
-rw-r--r--apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php28
-rw-r--r--apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php47
-rw-r--r--apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php47
-rw-r--r--apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php47
-rw-r--r--apps/dav/lib/Events/CalendarCreatedEvent.php35
-rw-r--r--apps/dav/lib/Events/CalendarDeletedEvent.php41
-rw-r--r--apps/dav/lib/Events/CalendarMovedToTrashEvent.php41
-rw-r--r--apps/dav/lib/Events/CalendarObjectCreatedEvent.php101
-rw-r--r--apps/dav/lib/Events/CalendarObjectDeletedEvent.php101
-rw-r--r--apps/dav/lib/Events/CalendarObjectMovedEvent.php120
-rw-r--r--apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php96
-rw-r--r--apps/dav/lib/Events/CalendarObjectRestoredEvent.php96
-rw-r--r--apps/dav/lib/Events/CalendarObjectUpdatedEvent.php101
-rw-r--r--apps/dav/lib/Events/CalendarPublishedEvent.php37
-rw-r--r--apps/dav/lib/Events/CalendarRestoredEvent.php41
-rw-r--r--apps/dav/lib/Events/CalendarShareUpdatedEvent.php63
-rw-r--r--apps/dav/lib/Events/CalendarUnpublishedEvent.php33
-rw-r--r--apps/dav/lib/Events/CalendarUpdatedEvent.php47
-rw-r--r--apps/dav/lib/Events/CardCreatedEvent.php47
-rw-r--r--apps/dav/lib/Events/CardDeletedEvent.php47
-rw-r--r--apps/dav/lib/Events/CardMovedEvent.php52
-rw-r--r--apps/dav/lib/Events/CardUpdatedEvent.php47
-rw-r--r--apps/dav/lib/Events/SabrePluginAddEvent.php29
-rw-r--r--apps/dav/lib/Events/SabrePluginAuthInitEvent.php29
-rw-r--r--apps/dav/lib/Events/SubscriptionCreatedEvent.php35
-rw-r--r--apps/dav/lib/Events/SubscriptionDeletedEvent.php41
-rw-r--r--apps/dav/lib/Events/SubscriptionUpdatedEvent.php47
-rw-r--r--apps/dav/lib/ExampleContentFiles/exampleContact.vcf3555
-rw-r--r--apps/dav/lib/Exception/ExampleEventException.php13
-rw-r--r--apps/dav/lib/Exception/ServerMaintenanceMode.php21
-rw-r--r--apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php22
-rw-r--r--apps/dav/lib/Files/BrowserErrorPagePlugin.php38
-rw-r--r--apps/dav/lib/Files/FileSearchBackend.php42
-rw-r--r--apps/dav/lib/Files/FilesHome.php40
-rw-r--r--apps/dav/lib/Files/LazySearchBackend.php22
-rw-r--r--apps/dav/lib/Files/RootCollection.php30
-rw-r--r--apps/dav/lib/Files/Sharing/FilesDropPlugin.php206
-rw-r--r--apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php26
-rw-r--r--apps/dav/lib/Files/Sharing/RootCollection.php32
-rw-r--r--apps/dav/lib/HookManager.php194
-rw-r--r--apps/dav/lib/Listener/ActivityUpdaterListener.php47
-rw-r--r--apps/dav/lib/Listener/AddMissingIndicesListener.php40
-rw-r--r--apps/dav/lib/Listener/AddressbookListener.php35
-rw-r--r--apps/dav/lib/Listener/BirthdayListener.php28
-rw-r--r--apps/dav/lib/Listener/CalendarContactInteractionListener.php57
-rw-r--r--apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php35
-rw-r--r--apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php57
-rw-r--r--apps/dav/lib/Listener/CalendarPublicationListener.php32
-rw-r--r--apps/dav/lib/Listener/CalendarShareUpdateListener.php36
-rw-r--r--apps/dav/lib/Listener/CardListener.php35
-rw-r--r--apps/dav/lib/Listener/ClearPhotoCacheListener.php28
-rw-r--r--apps/dav/lib/Listener/DavAdminSettingsListener.php65
-rw-r--r--apps/dav/lib/Listener/OutOfOfficeListener.php22
-rw-r--r--apps/dav/lib/Listener/SubscriptionListener.php38
-rw-r--r--apps/dav/lib/Listener/TrustedServerRemovedListener.php28
-rw-r--r--apps/dav/lib/Listener/UserEventsListener.php186
-rw-r--r--apps/dav/lib/Listener/UserPreferenceListener.php28
-rw-r--r--apps/dav/lib/Migration/BuildCalendarSearchIndex.php55
-rw-r--r--apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php31
-rw-r--r--apps/dav/lib/Migration/BuildSocialSearchIndex.php43
-rw-r--r--apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php32
-rw-r--r--apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php50
-rw-r--r--apps/dav/lib/Migration/ChunkCleanup.php47
-rw-r--r--apps/dav/lib/Migration/CreateSystemAddressBookStep.php30
-rw-r--r--apps/dav/lib/Migration/DeleteSchedulingObjects.php39
-rw-r--r--apps/dav/lib/Migration/FixBirthdayCalendarComponent.php36
-rw-r--r--apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php35
-rw-r--r--apps/dav/lib/Migration/RegenerateBirthdayCalendars.php37
-rw-r--r--apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php48
-rw-r--r--apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php30
-rw-r--r--apps/dav/lib/Migration/RemoveClassifiedEventActivity.php38
-rw-r--r--apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php36
-rw-r--r--apps/dav/lib/Migration/RemoveObjectProperties.php29
-rw-r--r--apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php116
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170825134824.php29
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170919104507.php24
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170924124212.php28
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170926103422.php25
-rw-r--r--apps/dav/lib/Migration/Version1005Date20180413093149.php23
-rw-r--r--apps/dav/lib/Migration/Version1005Date20180530124431.php26
-rw-r--r--apps/dav/lib/Migration/Version1006Date20180619154313.php26
-rw-r--r--apps/dav/lib/Migration/Version1006Date20180628111625.php26
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181030113700.php24
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105104826.php32
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105104833.php22
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105110300.php32
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181105112049.php22
-rw-r--r--apps/dav/lib/Migration/Version1008Date20181114084440.php22
-rw-r--r--apps/dav/lib/Migration/Version1011Date20190725113607.php24
-rw-r--r--apps/dav/lib/Migration/Version1011Date20190806104428.php24
-rw-r--r--apps/dav/lib/Migration/Version1012Date20190808122342.php25
-rw-r--r--apps/dav/lib/Migration/Version1016Date20201109085907.php21
-rw-r--r--apps/dav/lib/Migration/Version1017Date20210216083742.php21
-rw-r--r--apps/dav/lib/Migration/Version1018Date20210312100735.php21
-rw-r--r--apps/dav/lib/Migration/Version1024Date20211221144219.php5
-rw-r--r--apps/dav/lib/Migration/Version1025Date20240308063933.php53
-rw-r--r--apps/dav/lib/Migration/Version1027Date20230504122946.php29
-rw-r--r--apps/dav/lib/Migration/Version1029Date20221114151721.php23
-rw-r--r--apps/dav/lib/Migration/Version1029Date20231004091403.php21
-rw-r--r--apps/dav/lib/Migration/Version1030Date20240205103243.php21
-rw-r--r--apps/dav/lib/Migration/Version1031Date20240610134258.php48
-rw-r--r--apps/dav/lib/Model/ExampleEvent.php31
-rw-r--r--apps/dav/lib/Paginate/LimitedCopyIterator.php51
-rw-r--r--apps/dav/lib/Paginate/PaginateCache.php75
-rw-r--r--apps/dav/lib/Paginate/PaginatePlugin.php95
-rw-r--r--apps/dav/lib/Profiler/ProfilerPlugin.php28
-rw-r--r--apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php32
-rw-r--r--apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php79
-rw-r--r--apps/dav/lib/ResponseDefinitions.php34
-rw-r--r--apps/dav/lib/RootCollection.php112
-rw-r--r--apps/dav/lib/Search/ACalendarSearchProvider.php51
-rw-r--r--apps/dav/lib/Search/ContactsSearchProvider.php34
-rw-r--r--apps/dav/lib/Search/EventsSearchProvider.php36
-rw-r--r--apps/dav/lib/Search/TasksSearchProvider.php26
-rw-r--r--apps/dav/lib/Server.php271
-rw-r--r--apps/dav/lib/ServerFactory.php25
-rw-r--r--apps/dav/lib/Service/AbsenceService.php25
-rw-r--r--apps/dav/lib/Service/ExampleContactService.php132
-rw-r--r--apps/dav/lib/Service/ExampleEventService.php205
-rw-r--r--apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php43
-rw-r--r--apps/dav/lib/Settings/AvailabilitySettings.php40
-rw-r--r--apps/dav/lib/Settings/CalDAVSettings.php53
-rw-r--r--apps/dav/lib/Settings/ExampleContentSettings.php71
-rw-r--r--apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php22
-rw-r--r--apps/dav/lib/SetupChecks/WebdavEndpoint.php24
-rw-r--r--apps/dav/lib/Storage/PublicOwnerWrapper.php41
-rw-r--r--apps/dav/lib/Storage/PublicShareWrapper.php39
-rw-r--r--apps/dav/lib/SystemTag/SystemTagList.php43
-rw-r--r--apps/dav/lib/SystemTag/SystemTagMappingNode.php24
-rw-r--r--apps/dav/lib/SystemTag/SystemTagNode.php113
-rw-r--r--apps/dav/lib/SystemTag/SystemTagObjectType.php82
-rw-r--r--apps/dav/lib/SystemTag/SystemTagPlugin.php215
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsByIdCollection.php53
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsInUseCollection.php49
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectList.php85
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php26
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php26
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php29
-rw-r--r--apps/dav/lib/Traits/PrincipalProxyTrait.php23
-rw-r--r--apps/dav/lib/Upload/AssemblyStream.php51
-rw-r--r--apps/dav/lib/Upload/ChunkingPlugin.php29
-rw-r--r--apps/dav/lib/Upload/ChunkingV2Plugin.php28
-rw-r--r--apps/dav/lib/Upload/CleanupService.php41
-rw-r--r--apps/dav/lib/Upload/FutureFile.php36
-rw-r--r--apps/dav/lib/Upload/PartFile.php36
-rw-r--r--apps/dav/lib/Upload/RootCollection.php47
-rw-r--r--apps/dav/lib/Upload/UploadAutoMkcolPlugin.php68
-rw-r--r--apps/dav/lib/Upload/UploadFile.php29
-rw-r--r--apps/dav/lib/Upload/UploadFolder.php48
-rw-r--r--apps/dav/lib/Upload/UploadHome.php113
-rw-r--r--apps/dav/lib/UserMigration/CalendarMigrator.php48
-rw-r--r--apps/dav/lib/UserMigration/CalendarMigratorException.php21
-rw-r--r--apps/dav/lib/UserMigration/ContactsMigrator.php34
-rw-r--r--apps/dav/lib/UserMigration/ContactsMigratorException.php21
-rw-r--r--apps/dav/lib/UserMigration/InvalidAddressBookException.php21
-rw-r--r--apps/dav/lib/UserMigration/InvalidCalendarException.php21
410 files changed, 15460 insertions, 13383 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php
index deb28797952..9807b585080 100644
--- a/apps/dav/lib/AppInfo/Application.php
+++ b/apps/dav/lib/AppInfo/Application.php
@@ -3,37 +3,14 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobia De Koninck <tobia@ledfan.be>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\AppInfo;
-use OCA\DAV\CalDAV\Activity\Backend;
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
+use OCA\DAV\CalDAV\CachedSubscriptionProvider;
use OCA\DAV\CalDAV\CalendarManager;
use OCA\DAV\CalDAV\CalendarProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
@@ -41,10 +18,8 @@ use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
use OCA\DAV\CalDAV\Reminder\Notifier;
-
use OCA\DAV\Capabilities;
use OCA\DAV\CardDAV\ContactsManager;
-use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
use OCA\DAV\Events\AddressBookCreatedEvent;
use OCA\DAV\Events\AddressBookDeletedEvent;
@@ -53,12 +28,6 @@ use OCA\DAV\Events\AddressBookUpdatedEvent;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectDeletedEvent;
-use OCA\DAV\Events\CalendarObjectMovedEvent;
-use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectRestoredEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarPublishedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
@@ -69,8 +38,8 @@ use OCA\DAV\Events\CardDeletedEvent;
use OCA\DAV\Events\CardUpdatedEvent;
use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
-use OCA\DAV\HookManager;
use OCA\DAV\Listener\ActivityUpdaterListener;
+use OCA\DAV\Listener\AddMissingIndicesListener;
use OCA\DAV\Listener\AddressbookListener;
use OCA\DAV\Listener\BirthdayListener;
use OCA\DAV\Listener\CalendarContactInteractionListener;
@@ -80,37 +49,56 @@ use OCA\DAV\Listener\CalendarPublicationListener;
use OCA\DAV\Listener\CalendarShareUpdateListener;
use OCA\DAV\Listener\CardListener;
use OCA\DAV\Listener\ClearPhotoCacheListener;
+use OCA\DAV\Listener\DavAdminSettingsListener;
use OCA\DAV\Listener\OutOfOfficeListener;
use OCA\DAV\Listener\SubscriptionListener;
use OCA\DAV\Listener\TrustedServerRemovedListener;
+use OCA\DAV\Listener\UserEventsListener;
use OCA\DAV\Listener\UserPreferenceListener;
use OCA\DAV\Search\ContactsSearchProvider;
use OCA\DAV\Search\EventsSearchProvider;
use OCA\DAV\Search\TasksSearchProvider;
+use OCA\DAV\Settings\Admin\SystemAddressBookSettings;
use OCA\DAV\SetupChecks\NeedsSystemAddressBookSync;
use OCA\DAV\SetupChecks\WebdavEndpoint;
use OCA\DAV\UserMigration\CalendarMigrator;
use OCA\DAV\UserMigration\ContactsMigrator;
use OCP\Accounts\UserUpdatedEvent;
+use OCP\App\IAppManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\IAppContainer;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent;
+use OCP\Calendar\Events\CalendarObjectRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\Calendar\IManager as ICalendarManager;
use OCP\Config\BeforePreferenceDeletedEvent;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\Contacts\IManager as IContactsManager;
-use OCP\EventDispatcher\IEventDispatcher;
+use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\Federation\Events\TrustedServerRemovedEvent;
-use OCP\Files\AppData\IAppDataFactory;
-use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Server;
+use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
+use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
+use OCP\User\Events\BeforeUserDeletedEvent;
+use OCP\User\Events\BeforeUserIdUnassignedEvent;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\Events\UserChangedEvent;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserFirstTimeLoggedInEvent;
+use OCP\User\Events\UserIdAssignedEvent;
+use OCP\User\Events\UserIdUnassignedEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
use Throwable;
use function is_null;
@@ -123,12 +111,6 @@ class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
$context->registerServiceAlias('CardDAVSyncService', SyncService::class);
- $context->registerService(PhotoCache::class, function (ContainerInterface $c) {
- return new PhotoCache(
- $c->get(IAppDataFactory::class)->get('dav-photocache'),
- $c->get(LoggerInterface::class)
- );
- });
$context->registerService(AppCalendarPlugin::class, function (ContainerInterface $c) {
return new AppCalendarPlugin(
$c->get(ICalendarManager::class),
@@ -151,6 +133,8 @@ class Application extends App implements IBootstrap {
/**
* Register event listeners
*/
+ $context->registerEventListener(AddMissingIndicesEvent::class, AddMissingIndicesListener::class);
+
$context->registerEventListener(CalendarCreatedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarDeletedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarDeletedEvent::class, CalendarObjectReminderUpdaterListener::class);
@@ -204,63 +188,46 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);
+ $context->registerEventListener(UserFirstTimeLoggedInEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserIdAssignedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(BeforeUserIdUnassignedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserIdUnassignedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(BeforeUserDeletedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserDeletedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserCreatedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserChangedEvent::class, UserEventsListener::class);
+ $context->registerEventListener(UserUpdatedEvent::class, UserEventsListener::class);
+
$context->registerNotifierService(Notifier::class);
$context->registerCalendarProvider(CalendarProvider::class);
+ $context->registerCalendarProvider(CachedSubscriptionProvider::class);
$context->registerUserMigrator(CalendarMigrator::class);
$context->registerUserMigrator(ContactsMigrator::class);
$context->registerSetupCheck(NeedsSystemAddressBookSync::class);
$context->registerSetupCheck(WebdavEndpoint::class);
- }
- public function boot(IBootContext $context): void {
- // Load all dav apps
- \OC_App::loadApps(['dav']);
+ // register admin settings form and listener(s)
+ $context->registerDeclarativeSettings(SystemAddressBookSettings::class);
+ $context->registerEventListener(DeclarativeSettingsGetValueEvent::class, DavAdminSettingsListener::class);
+ $context->registerEventListener(DeclarativeSettingsSetValueEvent::class, DavAdminSettingsListener::class);
- $context->injectFn([$this, 'registerHooks']);
- $context->injectFn([$this, 'registerContactsManager']);
- $context->injectFn([$this, 'registerCalendarManager']);
- $context->injectFn([$this, 'registerCalendarReminders']);
}
- public function registerHooks(HookManager $hm,
- IEventDispatcher $dispatcher,
- IAppContainer $container) {
- $hm->setup();
-
- // first time login event setup
- $dispatcher->addListener(IUser::class . '::firstLogin', function ($event) use ($hm) {
- if ($event instanceof GenericEvent) {
- $hm->firstLogin($event->getSubject());
- }
- });
-
- $dispatcher->addListener(UserUpdatedEvent::class, function (UserUpdatedEvent $event) use ($container) {
- /** @var SyncService $syncService */
- $syncService = \OCP\Server::get(SyncService::class);
- $syncService->updateUser($event->getUser());
- });
-
-
- $dispatcher->addListener(CalendarShareUpdatedEvent::class, function (CalendarShareUpdatedEvent $event) use ($container) {
- /** @var Backend $backend */
- $backend = $container->query(Backend::class);
- $backend->onCalendarUpdateShares(
- $event->getCalendarData(),
- $event->getOldShares(),
- $event->getAdded(),
- $event->getRemoved()
- );
+ public function boot(IBootContext $context): void {
+ // Load all dav apps
+ $context->getServerContainer()->get(IAppManager::class)->loadApps(['dav']);
- // Here we should recalculate if reminders should be sent to new or old sharees
- });
+ $context->injectFn($this->registerContactsManager(...));
+ $context->injectFn($this->registerCalendarManager(...));
+ $context->injectFn($this->registerCalendarReminders(...));
}
public function registerContactsManager(IContactsManager $cm, IAppContainer $container): void {
$cm->register(function () use ($container, $cm): void {
- $user = \OC::$server->getUserSession()->getUser();
+ $user = Server::get(IUserSession::class)->getUser();
if (!is_null($user)) {
$this->setupContactsProvider($cm, $container, $user->getUID());
} else {
@@ -278,18 +245,17 @@ class Application extends App implements IBootstrap {
$cm->setupContactsProvider($contactsManager, $userID, $urlGenerator);
}
- private function setupSystemContactsProvider(IContactsManager $contactsManager,
- IAppContainer $container): void {
+ private function setupSystemContactsProvider(IContactsManager $contactsManager, IAppContainer $container): void {
/** @var ContactsManager $cm */
$cm = $container->query(ContactsManager::class);
$urlGenerator = $container->getServer()->getURLGenerator();
- $cm->setupSystemContactsProvider($contactsManager, $urlGenerator);
+ $cm->setupSystemContactsProvider($contactsManager, null, $urlGenerator);
}
public function registerCalendarManager(ICalendarManager $calendarManager,
IAppContainer $container): void {
- $calendarManager->register(function () use ($container, $calendarManager) {
- $user = \OC::$server->getUserSession()->getUser();
+ $calendarManager->register(function () use ($container, $calendarManager): void {
+ $user = Server::get(IUserSession::class)->getUser();
if ($user !== null) {
$this->setupCalendarProvider($calendarManager, $container, $user->getUID());
}
diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php
index 828818455f7..428547e3f61 100644
--- a/apps/dav/lib/AppInfo/PluginManager.php
+++ b/apps/dav/lib/AppInfo/PluginManager.php
@@ -3,28 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\AppInfo;
@@ -47,16 +28,6 @@ use function is_array;
class PluginManager {
/**
- * @var ServerContainer
- */
- private $container;
-
- /**
- * @var IAppManager
- */
- private $appManager;
-
- /**
* App plugins
*
* @var ServerPlugin[]
@@ -93,9 +64,10 @@ class PluginManager {
* @param ServerContainer $container server container for resolving plugin classes
* @param IAppManager $appManager app manager to loading apps and their info
*/
- public function __construct(ServerContainer $container, IAppManager $appManager) {
- $this->container = $container;
- $this->appManager = $appManager;
+ public function __construct(
+ private ServerContainer $container,
+ private IAppManager $appManager,
+ ) {
}
/**
@@ -147,7 +119,7 @@ class PluginManager {
$this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class);
- foreach ($this->appManager->getInstalledApps() as $app) {
+ foreach ($this->appManager->getEnabledApps() as $app) {
// load plugins and collections from info.xml
$info = $this->appManager->getAppInfo($app);
if (!isset($info['types']) || !in_array('dav', $info['types'], true)) {
diff --git a/apps/dav/lib/Avatars/AvatarHome.php b/apps/dav/lib/Avatars/AvatarHome.php
index 097307f452f..c3b95db1f4f 100644
--- a/apps/dav/lib/Avatars/AvatarHome.php
+++ b/apps/dav/lib/Avatars/AvatarHome.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Avatars;
@@ -33,20 +16,16 @@ use Sabre\Uri;
class AvatarHome implements ICollection {
- /** @var array */
- private $principalInfo;
- /** @var IAvatarManager */
- private $avatarManager;
-
/**
* AvatarHome constructor.
*
* @param array $principalInfo
* @param IAvatarManager $avatarManager
*/
- public function __construct($principalInfo, IAvatarManager $avatarManager) {
- $this->principalInfo = $principalInfo;
- $this->avatarManager = $avatarManager;
+ public function __construct(
+ private $principalInfo,
+ private IAvatarManager $avatarManager,
+ ) {
}
public function createFile($name, $data = null) {
diff --git a/apps/dav/lib/Avatars/AvatarNode.php b/apps/dav/lib/Avatars/AvatarNode.php
index ade523561f2..b3a605fbb02 100644
--- a/apps/dav/lib/Avatars/AvatarNode.php
+++ b/apps/dav/lib/Avatars/AvatarNode.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Avatars;
@@ -26,10 +11,6 @@ use OCP\IAvatar;
use Sabre\DAV\File;
class AvatarNode extends File {
- private $ext;
- private $size;
- private $avatar;
-
/**
* AvatarNode constructor.
*
@@ -37,10 +18,11 @@ class AvatarNode extends File {
* @param string $ext
* @param IAvatar $avatar
*/
- public function __construct($size, $ext, $avatar) {
- $this->size = $size;
- $this->ext = $ext;
- $this->avatar = $avatar;
+ public function __construct(
+ private $size,
+ private $ext,
+ private $avatar,
+ ) {
}
/**
@@ -87,10 +69,6 @@ class AvatarNode extends File {
}
public function getLastModified() {
- $timestamp = $this->avatar->getFile($this->size)->getMTime();
- if (!empty($timestamp)) {
- return (int)$timestamp;
- }
- return $timestamp;
+ return $this->avatar->getFile($this->size)->getMTime();
}
}
diff --git a/apps/dav/lib/Avatars/RootCollection.php b/apps/dav/lib/Avatars/RootCollection.php
index c5e78624d44..033dcaf7a5c 100644
--- a/apps/dav/lib/Avatars/RootCollection.php
+++ b/apps/dav/lib/Avatars/RootCollection.php
@@ -1,29 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Avatars;
+use OCP\IAvatarManager;
+use OCP\Server;
use Sabre\DAVACL\AbstractPrincipalCollection;
class RootCollection extends AbstractPrincipalCollection {
@@ -39,7 +24,7 @@ class RootCollection extends AbstractPrincipalCollection {
* @return AvatarHome
*/
public function getChildForPrincipal(array $principalInfo) {
- $avatarManager = \OC::$server->getAvatarManager();
+ $avatarManager = Server::get(IAvatarManager::class);
return new AvatarHome($principalInfo, $avatarManager);
}
diff --git a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php
index d1cafbf57c2..1165367c33f 100644
--- a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php
+++ b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -41,41 +22,28 @@ use Psr\Log\LoggerInterface;
*/
class BuildReminderIndexBackgroundJob extends QueuedJob {
- /** @var IDBConnection */
- private $db;
-
- /** @var ReminderService */
- private $reminderService;
-
- private LoggerInterface $logger;
-
- /** @var IJobList */
- private $jobList;
-
/** @var ITimeFactory */
private $timeFactory;
/**
* BuildReminderIndexBackgroundJob constructor.
*/
- public function __construct(IDBConnection $db,
- ReminderService $reminderService,
- LoggerInterface $logger,
- IJobList $jobList,
- ITimeFactory $timeFactory) {
+ public function __construct(
+ private IDBConnection $db,
+ private ReminderService $reminderService,
+ private LoggerInterface $logger,
+ private IJobList $jobList,
+ ITimeFactory $timeFactory,
+ ) {
parent::__construct($timeFactory);
- $this->db = $db;
- $this->reminderService = $reminderService;
- $this->logger = $logger;
- $this->jobList = $jobList;
$this->timeFactory = $timeFactory;
}
public function run($argument) {
- $offset = (int) $argument['offset'];
- $stopAt = (int) $argument['stopAt'];
+ $offset = (int)$argument['offset'];
+ $stopAt = (int)$argument['stopAt'];
- $this->logger->info('Building calendar reminder index (' . $offset .'/' . $stopAt . ')');
+ $this->logger->info('Building calendar reminder index (' . $offset . '/' . $stopAt . ')');
$offset = $this->buildIndex($offset, $stopAt);
@@ -107,7 +75,7 @@ class BuildReminderIndexBackgroundJob extends QueuedJob {
$result = $query->executeQuery();
while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
- $offset = (int) $row['id'];
+ $offset = (int)$row['id'];
if (is_resource($row['calendardata'])) {
$row['calendardata'] = stream_get_contents($row['calendardata']);
}
diff --git a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php
index 96ceb644489..c6edac4f228 100644
--- a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php
+++ b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -30,13 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
class CalendarRetentionJob extends TimedJob {
- /** @var RetentionService */
- private $service;
-
- public function __construct(ITimeFactory $time,
- RetentionService $service) {
+ public function __construct(
+ ITimeFactory $time,
+ private RetentionService $service,
+ ) {
parent::__construct($time);
- $this->service = $service;
// Run four times a day
$this->setInterval(6 * 60 * 60);
diff --git a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php
index 073fc53e07a..49b6b1607ef 100644
--- a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php
+++ b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -31,12 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
class CleanupDirectLinksJob extends TimedJob {
- /** @var DirectMapper */
- private $mapper;
-
- public function __construct(ITimeFactory $timeFactory, DirectMapper $mapper) {
+ public function __construct(
+ ITimeFactory $timeFactory,
+ private DirectMapper $mapper,
+ ) {
parent::__construct($timeFactory);
- $this->mapper = $mapper;
// Run once a day at off-peak time
$this->setInterval(24 * 60 * 60);
diff --git a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php
index 6339e721c93..7b664d03181 100644
--- a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php
+++ b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -32,12 +14,11 @@ use OCP\IDBConnection;
class CleanupInvitationTokenJob extends TimedJob {
- /** @var IDBConnection */
- private $db;
-
- public function __construct(IDBConnection $db, ITimeFactory $time) {
+ public function __construct(
+ private IDBConnection $db,
+ ITimeFactory $time,
+ ) {
parent::__construct($time);
- $this->db = $db;
// Run once a day at off-peak time
$this->setInterval(24 * 60 * 60);
diff --git a/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php
new file mode 100644
index 00000000000..8a5e34381a7
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
+
+class CleanupOrphanedChildrenJob extends QueuedJob {
+ public const ARGUMENT_CHILD_TABLE = 'childTable';
+ public const ARGUMENT_PARENT_TABLE = 'parentTable';
+ public const ARGUMENT_PARENT_ID = 'parentId';
+ public const ARGUMENT_LOG_MESSAGE = 'logMessage';
+
+ private const BATCH_SIZE = 1000;
+
+ public function __construct(
+ ITimeFactory $time,
+ private readonly IDBConnection $connection,
+ private readonly LoggerInterface $logger,
+ private readonly IJobList $jobList,
+ ) {
+ parent::__construct($time);
+ }
+
+ protected function run($argument): void {
+ $childTable = $argument[self::ARGUMENT_CHILD_TABLE];
+ $parentTable = $argument[self::ARGUMENT_PARENT_TABLE];
+ $parentId = $argument[self::ARGUMENT_PARENT_ID];
+ $logMessage = $argument[self::ARGUMENT_LOG_MESSAGE];
+
+ $orphanCount = $this->cleanUpOrphans($childTable, $parentTable, $parentId);
+ $this->logger->debug(sprintf($logMessage, $orphanCount));
+
+ // Requeue if there might be more orphans
+ if ($orphanCount >= self::BATCH_SIZE) {
+ $this->jobList->add(self::class, $argument);
+ }
+ }
+
+ private function cleanUpOrphans(
+ string $childTable,
+ string $parentTable,
+ string $parentId,
+ ): int {
+ // We can't merge both queries into a single one here as DELETEing from a table while
+ // SELECTing it in a sub query is not supported by Oracle DB.
+ // Ref https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/delete.html#idm46006185488144
+
+ $selectQb = $this->connection->getQueryBuilder();
+
+ $selectQb->select('c.id')
+ ->from($childTable, 'c')
+ ->leftJoin('c', $parentTable, 'p', $selectQb->expr()->eq('c.' . $parentId, 'p.id'))
+ ->where($selectQb->expr()->isNull('p.id'))
+ ->setMaxResults(self::BATCH_SIZE);
+
+ if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) {
+ $calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR;
+ $selectQb->andWhere($selectQb->expr()->eq('c.calendartype', $selectQb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ }
+
+ $result = $selectQb->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+ if (empty($rows)) {
+ return 0;
+ }
+
+ $orphanItems = array_map(static fn ($row) => $row['id'], $rows);
+ $deleteQb = $this->connection->getQueryBuilder();
+ $deleteQb->delete($childTable)
+ ->where($deleteQb->expr()->in('id', $deleteQb->createNamedParameter($orphanItems, IQueryBuilder::PARAM_INT_ARRAY)));
+ $deleteQb->executeStatement();
+
+ return count($orphanItems);
+ }
+}
diff --git a/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php
new file mode 100644
index 00000000000..bc306d58fe1
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use Psr\Log\LoggerInterface;
+
+class DeleteOutdatedSchedulingObjects extends TimedJob {
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ ITimeFactory $timeFactory,
+ ) {
+ parent::__construct($timeFactory);
+ $this->setInterval(23 * 60 * 60);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
+ }
+
+ /**
+ * @param array $argument
+ */
+ protected function run($argument): void {
+ $time = $this->time->getTime() - (60 * 60);
+ $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000);
+ $this->logger->info('Removed outdated scheduling objects');
+ }
+}
diff --git a/apps/dav/lib/BackgroundJob/EventReminderJob.php b/apps/dav/lib/BackgroundJob/EventReminderJob.php
index f628a728404..0e21e06fc35 100644
--- a/apps/dav/lib/BackgroundJob/EventReminderJob.php
+++ b/apps/dav/lib/BackgroundJob/EventReminderJob.php
@@ -3,29 +3,14 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
+use OC\User\NoUserException;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException;
+use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException;
use OCA\DAV\CalDAV\Reminder\ReminderService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
@@ -33,18 +18,12 @@ use OCP\IConfig;
class EventReminderJob extends TimedJob {
- /** @var ReminderService */
- private $reminderService;
-
- /** @var IConfig */
- private $config;
-
- public function __construct(ITimeFactory $time,
- ReminderService $reminderService,
- IConfig $config) {
+ public function __construct(
+ ITimeFactory $time,
+ private ReminderService $reminderService,
+ private IConfig $config,
+ ) {
parent::__construct($time);
- $this->reminderService = $reminderService;
- $this->config = $config;
// Run every 5 minutes
$this->setInterval(5 * 60);
@@ -52,9 +31,9 @@ class EventReminderJob extends TimedJob {
}
/**
- * @throws \OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException
- * @throws \OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException
- * @throws \OC\User\NoUserException
+ * @throws ProviderNotAvailableException
+ * @throws NotificationTypeDoesNotExistException
+ * @throws NoUserException
*/
public function run($argument):void {
if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') {
diff --git a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php
index 220050b3927..6d94f4810ed 100644
--- a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php
+++ b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -32,26 +15,19 @@ use OCP\IConfig;
class GenerateBirthdayCalendarBackgroundJob extends QueuedJob {
- /** @var BirthdayService */
- private $birthdayService;
-
- /** @var IConfig */
- private $config;
-
- public function __construct(ITimeFactory $time,
- BirthdayService $birthdayService,
- IConfig $config) {
+ public function __construct(
+ ITimeFactory $time,
+ private BirthdayService $birthdayService,
+ private IConfig $config,
+ ) {
parent::__construct($time);
-
- $this->birthdayService = $birthdayService;
- $this->config = $config;
}
public function run($argument) {
$userId = $argument['userId'];
$purgeBeforeGenerating = $argument['purgeBeforeGenerating'] ?? false;
- // make sure admin didn't change his mind
+ // make sure admin didn't change their mind
$isGloballyEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes');
if ($isGloballyEnabled !== 'yes') {
return;
diff --git a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php
index 9b219cf30da..cc4fd5dce9d 100644
--- a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php
+++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -58,7 +41,7 @@ class OutOfOfficeEventDispatcherJob extends QueuedJob {
try {
$absence = $this->absenceMapper->findById($id);
- } catch (DoesNotExistException | \OCP\DB\Exception $e) {
+ } catch (DoesNotExistException|\OCP\DB\Exception $e) {
$this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [
'exception' => $e,
'argument' => $argument,
diff --git a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php
index d5a877a1742..8746588acc7 100644
--- a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php
+++ b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -35,24 +18,21 @@ use Psr\Log\LoggerInterface;
class PruneOutdatedSyncTokensJob extends TimedJob {
- private IConfig $config;
- private LoggerInterface $logger;
- private CardDavBackend $cardDavBackend;
- private CalDavBackend $calDavBackend;
-
- public function __construct(ITimeFactory $timeFactory, CalDavBackend $calDavBackend, CardDavBackend $cardDavBackend, IConfig $config, LoggerInterface $logger) {
+ public function __construct(
+ ITimeFactory $timeFactory,
+ private CalDavBackend $calDavBackend,
+ private CardDavBackend $cardDavBackend,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($timeFactory);
- $this->calDavBackend = $calDavBackend;
- $this->cardDavBackend = $cardDavBackend;
- $this->config = $config;
- $this->logger = $logger;
$this->setInterval(60 * 60 * 24); // One day
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
public function run($argument) {
- $limit = max(1, (int) $this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000'));
- $retention = max(7, (int) $this->config->getAppValue(Application::APP_ID, 'syncTokensRetentionDays', '60')) * 24 * 3600;
+ $limit = max(1, (int)$this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000'));
+ $retention = max(7, (int)$this->config->getAppValue(Application::APP_ID, 'syncTokensRetentionDays', '60')) * 24 * 3600;
$prunedCalendarSyncTokens = $this->calDavBackend->pruneOutdatedSyncTokens($limit, $retention);
$prunedAddressBookSyncTokens = $this->cardDavBackend->pruneOutdatedSyncTokens($limit, $retention);
diff --git a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
index a40aeee3d66..e96735ca50b 100644
--- a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
+++ b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -62,8 +41,8 @@ class RefreshWebcalJob extends Job {
$this->fixSubscriptionRowTyping($subscription);
- // if no refresh rate was configured, just refresh once a week
- $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1W');
+ // if no refresh rate was configured, just refresh once a day
+ $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1D');
$refreshRate = $subscription[RefreshWebcalService::REFRESH_RATE] ?? $defaultRefreshRate;
$subscriptionId = $subscription['id'];
@@ -125,7 +104,7 @@ class RefreshWebcalJob extends Job {
foreach ($forceInt as $column) {
if (isset($row[$column])) {
- $row[$column] = (int) $row[$column];
+ $row[$column] = (int)$row[$column];
}
}
}
diff --git a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php
index 8c0b5e3ea45..7ec5b7fba79 100644
--- a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php
+++ b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -35,12 +16,6 @@ use OCP\IUserManager;
class RegisterRegenerateBirthdayCalendars extends QueuedJob {
- /** @var IUserManager */
- private $userManager;
-
- /** @var IJobList */
- private $jobList;
-
/**
* RegisterRegenerateBirthdayCalendars constructor.
*
@@ -48,19 +23,19 @@ class RegisterRegenerateBirthdayCalendars extends QueuedJob {
* @param IUserManager $userManager
* @param IJobList $jobList
*/
- public function __construct(ITimeFactory $time,
- IUserManager $userManager,
- IJobList $jobList) {
+ public function __construct(
+ ITimeFactory $time,
+ private IUserManager $userManager,
+ private IJobList $jobList,
+ ) {
parent::__construct($time);
- $this->userManager = $userManager;
- $this->jobList = $jobList;
}
/**
* @inheritDoc
*/
public function run($argument) {
- $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->userManager->callForSeenUsers(function (IUser $user): void {
$this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [
'userId' => $user->getUID(),
'purgeBeforeGenerating' => true
diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php
index b4571e2509d..b7688ea32d8 100644
--- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php
+++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php
@@ -3,452 +3,32 @@
declare(strict_types=1);
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
-use OCA\DAV\CalDAV\CalDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
-use OCP\Calendar\BackendTemporarilyUnavailableException;
-use OCP\Calendar\IMetadataProvider;
-use OCP\Calendar\Resource\IBackend as IResourceBackend;
use OCP\Calendar\Resource\IManager as IResourceManager;
-use OCP\Calendar\Resource\IResource;
use OCP\Calendar\Room\IManager as IRoomManager;
-use OCP\Calendar\Room\IRoom;
-use OCP\IDBConnection;
class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
-
- /** @var IResourceManager */
- private $resourceManager;
-
- /** @var IRoomManager */
- private $roomManager;
-
- /** @var IDBConnection */
- private $dbConnection;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- public function __construct(ITimeFactory $time,
- IResourceManager $resourceManager,
- IRoomManager $roomManager,
- IDBConnection $dbConnection,
- CalDavBackend $calDavBackend) {
+ public function __construct(
+ ITimeFactory $time,
+ private IResourceManager $resourceManager,
+ private IRoomManager $roomManager,
+ ) {
parent::__construct($time);
- $this->resourceManager = $resourceManager;
- $this->roomManager = $roomManager;
- $this->dbConnection = $dbConnection;
- $this->calDavBackend = $calDavBackend;
// Run once an hour
$this->setInterval(60 * 60);
$this->setTimeSensitivity(self::TIME_SENSITIVE);
}
- /**
- * @param $argument
- */
public function run($argument): void {
- $this->runForBackend(
- $this->resourceManager,
- 'calendar_resources',
- 'calendar_resources_md',
- 'resource_id',
- 'principals/calendar-resources'
- );
- $this->runForBackend(
- $this->roomManager,
- 'calendar_rooms',
- 'calendar_rooms_md',
- 'room_id',
- 'principals/calendar-rooms'
- );
- }
-
- /**
- * Run background-job for one specific backendManager
- * either ResourceManager or RoomManager
- *
- * @param IResourceManager|IRoomManager $backendManager
- * @param string $dbTable
- * @param string $dbTableMetadata
- * @param string $foreignKey
- * @param string $principalPrefix
- */
- private function runForBackend($backendManager,
- string $dbTable,
- string $dbTableMetadata,
- string $foreignKey,
- string $principalPrefix): void {
- $backends = $backendManager->getBackends();
-
- foreach ($backends as $backend) {
- $backendId = $backend->getBackendIdentifier();
-
- try {
- if ($backend instanceof IResourceBackend) {
- $list = $backend->listAllResources();
- } else {
- $list = $backend->listAllRooms();
- }
- } catch (BackendTemporarilyUnavailableException $ex) {
- continue;
- }
-
- $cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
- $newIds = array_diff($list, $cachedList);
- $deletedIds = array_diff($cachedList, $list);
- $editedIds = array_intersect($list, $cachedList);
-
- foreach ($newIds as $newId) {
- try {
- if ($backend instanceof IResourceBackend) {
- $resource = $backend->getResource($newId);
- } else {
- $resource = $backend->getRoom($newId);
- }
-
- $metadata = [];
- if ($resource instanceof IMetadataProvider) {
- $metadata = $this->getAllMetadataOfBackend($resource);
- }
- } catch (BackendTemporarilyUnavailableException $ex) {
- continue;
- }
-
- $id = $this->addToCache($dbTable, $backendId, $resource);
- $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
- // we don't create the calendar here, it is created lazily
- // when an event is actually scheduled with this resource / room
- }
-
- foreach ($deletedIds as $deletedId) {
- $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
- $this->deleteFromCache($dbTable, $id);
- $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
-
- $principalName = implode('-', [$backendId, $deletedId]);
- $this->deleteCalendarDataForResource($principalPrefix, $principalName);
- }
-
- foreach ($editedIds as $editedId) {
- $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
-
- try {
- if ($backend instanceof IResourceBackend) {
- $resource = $backend->getResource($editedId);
- } else {
- $resource = $backend->getRoom($editedId);
- }
-
- $metadata = [];
- if ($resource instanceof IMetadataProvider) {
- $metadata = $this->getAllMetadataOfBackend($resource);
- }
- } catch (BackendTemporarilyUnavailableException $ex) {
- continue;
- }
-
- $this->updateCache($dbTable, $id, $resource);
-
- if ($resource instanceof IMetadataProvider) {
- $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
- $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
- }
- }
- }
- }
-
- /**
- * add entry to cache that exists remotely but not yet in cache
- *
- * @param string $table
- * @param string $backendId
- * @param IResource|IRoom $remote
- *
- * @return int Insert id
- */
- private function addToCache(string $table,
- string $backendId,
- $remote): int {
- $query = $this->dbConnection->getQueryBuilder();
- $query->insert($table)
- ->values([
- 'backend_id' => $query->createNamedParameter($backendId),
- 'resource_id' => $query->createNamedParameter($remote->getId()),
- 'email' => $query->createNamedParameter($remote->getEMail()),
- 'displayname' => $query->createNamedParameter($remote->getDisplayName()),
- 'group_restrictions' => $query->createNamedParameter(
- $this->serializeGroupRestrictions(
- $remote->getGroupRestrictions()
- ))
- ])
- ->executeStatement();
- return $query->getLastInsertId();
- }
-
- /**
- * @param string $table
- * @param string $foreignKey
- * @param int $foreignId
- * @param array $metadata
- */
- private function addMetadataToCache(string $table,
- string $foreignKey,
- int $foreignId,
- array $metadata): void {
- foreach ($metadata as $key => $value) {
- $query = $this->dbConnection->getQueryBuilder();
- $query->insert($table)
- ->values([
- $foreignKey => $query->createNamedParameter($foreignId),
- 'key' => $query->createNamedParameter($key),
- 'value' => $query->createNamedParameter($value),
- ])
- ->executeStatement();
- }
- }
-
- /**
- * delete entry from cache that does not exist anymore remotely
- *
- * @param string $table
- * @param int $id
- */
- private function deleteFromCache(string $table,
- int $id): void {
- $query = $this->dbConnection->getQueryBuilder();
- $query->delete($table)
- ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
- ->executeStatement();
- }
-
- /**
- * @param string $table
- * @param string $foreignKey
- * @param int $id
- */
- private function deleteMetadataFromCache(string $table,
- string $foreignKey,
- int $id): void {
- $query = $this->dbConnection->getQueryBuilder();
- $query->delete($table)
- ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
- ->executeStatement();
- }
-
- /**
- * update an existing entry in cache
- *
- * @param string $table
- * @param int $id
- * @param IResource|IRoom $remote
- */
- private function updateCache(string $table,
- int $id,
- $remote): void {
- $query = $this->dbConnection->getQueryBuilder();
- $query->update($table)
- ->set('email', $query->createNamedParameter($remote->getEMail()))
- ->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
- ->set('group_restrictions', $query->createNamedParameter(
- $this->serializeGroupRestrictions(
- $remote->getGroupRestrictions()
- )))
- ->where($query->expr()->eq('id', $query->createNamedParameter($id)))
- ->executeStatement();
- }
-
- /**
- * @param string $dbTable
- * @param string $foreignKey
- * @param int $id
- * @param array $metadata
- * @param array $cachedMetadata
- */
- private function updateMetadataCache(string $dbTable,
- string $foreignKey,
- int $id,
- array $metadata,
- array $cachedMetadata): void {
- $newMetadata = array_diff_key($metadata, $cachedMetadata);
- $deletedMetadata = array_diff_key($cachedMetadata, $metadata);
-
- foreach ($newMetadata as $key => $value) {
- $query = $this->dbConnection->getQueryBuilder();
- $query->insert($dbTable)
- ->values([
- $foreignKey => $query->createNamedParameter($id),
- 'key' => $query->createNamedParameter($key),
- 'value' => $query->createNamedParameter($value),
- ])
- ->executeStatement();
- }
-
- foreach ($deletedMetadata as $key => $value) {
- $query = $this->dbConnection->getQueryBuilder();
- $query->delete($dbTable)
- ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
- ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
- ->executeStatement();
- }
-
- $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
- foreach ($existingKeys as $existingKey) {
- if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
- $query = $this->dbConnection->getQueryBuilder();
- $query->update($dbTable)
- ->set('value', $query->createNamedParameter($metadata[$existingKey]))
- ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
- ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
- ->executeStatement();
- }
- }
- }
-
- /**
- * serialize array of group restrictions to store them in database
- *
- * @param array $groups
- *
- * @return string
- */
- private function serializeGroupRestrictions(array $groups): string {
- return \json_encode($groups, JSON_THROW_ON_ERROR);
- }
-
- /**
- * Gets all metadata of a backend
- *
- * @param IResource|IRoom $resource
- *
- * @return array
- */
- private function getAllMetadataOfBackend($resource): array {
- if (!($resource instanceof IMetadataProvider)) {
- return [];
- }
-
- $keys = $resource->getAllAvailableMetadataKeys();
- $metadata = [];
- foreach ($keys as $key) {
- $metadata[$key] = $resource->getMetadataForKey($key);
- }
-
- return $metadata;
- }
-
- /**
- * @param string $table
- * @param string $foreignKey
- * @param int $id
- *
- * @return array
- */
- private function getAllMetadataOfCache(string $table,
- string $foreignKey,
- int $id): array {
- $query = $this->dbConnection->getQueryBuilder();
- $query->select(['key', 'value'])
- ->from($table)
- ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
- $result = $query->executeQuery();
- $rows = $result->fetchAll();
- $result->closeCursor();
-
- $metadata = [];
- foreach ($rows as $row) {
- $metadata[$row['key']] = $row['value'];
- }
-
- return $metadata;
- }
-
- /**
- * Gets all cached rooms / resources by backend
- *
- * @param $tableName
- * @param $backendId
- *
- * @return array
- */
- private function getAllCachedByBackend(string $tableName,
- string $backendId): array {
- $query = $this->dbConnection->getQueryBuilder();
- $query->select('resource_id')
- ->from($tableName)
- ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
- $result = $query->executeQuery();
- $rows = $result->fetchAll();
- $result->closeCursor();
-
- return array_map(function ($row): string {
- return $row['resource_id'];
- }, $rows);
- }
-
- /**
- * @param $principalPrefix
- * @param $principalUri
- */
- private function deleteCalendarDataForResource(string $principalPrefix,
- string $principalUri): void {
- $calendar = $this->calDavBackend->getCalendarByUri(
- implode('/', [$principalPrefix, $principalUri]),
- CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
-
- if ($calendar !== null) {
- $this->calDavBackend->deleteCalendar(
- $calendar['id'],
- true // Because this wasn't deleted by a user
- );
- }
- }
-
- /**
- * @param $table
- * @param $backendId
- * @param $resourceId
- *
- * @return int
- */
- private function getIdForBackendAndResource(string $table,
- string $backendId,
- string $resourceId): int {
- $query = $this->dbConnection->getQueryBuilder();
- $query->select('id')
- ->from($table)
- ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
- ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
- $result = $query->executeQuery();
-
- $id = (int) $result->fetchOne();
- $result->closeCursor();
- return $id;
+ $this->resourceManager->update();
+ $this->roomManager->update();
}
}
diff --git a/apps/dav/lib/BackgroundJob/UploadCleanup.php b/apps/dav/lib/BackgroundJob/UploadCleanup.php
index c35aff4d15a..230cde61578 100644
--- a/apps/dav/lib/BackgroundJob/UploadCleanup.php
+++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php
@@ -3,33 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
use OC\User\NoUserException;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\File;
@@ -39,19 +19,17 @@ use OCP\Files\NotFoundException;
use Psr\Log\LoggerInterface;
class UploadCleanup extends TimedJob {
- private IRootFolder $rootFolder;
- private IJobList $jobList;
- private LoggerInterface $logger;
-
- public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList, LoggerInterface $logger) {
+ public function __construct(
+ ITimeFactory $time,
+ private IRootFolder $rootFolder,
+ private IJobList $jobList,
+ private LoggerInterface $logger,
+ ) {
parent::__construct($time);
- $this->rootFolder = $rootFolder;
- $this->jobList = $jobList;
- $this->logger = $logger;
// Run once a day
$this->setInterval(60 * 60 * 24);
- $this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
protected function run($argument) {
@@ -64,7 +42,7 @@ class UploadCleanup extends TimedJob {
/** @var Folder $uploads */
$uploads = $userRoot->get('uploads');
$uploadFolder = $uploads->get($folder);
- } catch (NotFoundException | NoUserException $e) {
+ } catch (NotFoundException|NoUserException $e) {
$this->jobList->remove(self::class, $argument);
return;
}
@@ -73,7 +51,7 @@ class UploadCleanup extends TimedJob {
$time = $this->time->getTime() - 60 * 60 * 24;
if (!($uploadFolder instanceof Folder)) {
- $this->logger->error("Found a file inside the uploads folder. Uid: " . $uid . ' folder: ' . $folder);
+ $this->logger->error('Found a file inside the uploads folder. Uid: ' . $uid . ' folder: ' . $folder);
if ($uploadFolder->getMTime() < $time) {
$uploadFolder->delete();
}
diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php
index 5f88fa122b7..027b3349802 100644
--- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php
+++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php
@@ -2,23 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\BackgroundJob;
@@ -43,18 +28,21 @@ use Sabre\VObject\Reader;
use Sabre\VObject\Recur\RRuleIterator;
class UserStatusAutomation extends TimedJob {
- public function __construct(private ITimeFactory $timeFactory,
+ public function __construct(
+ private ITimeFactory $timeFactory,
private IDBConnection $connection,
private IJobList $jobList,
private LoggerInterface $logger,
private IManager $manager,
private IConfig $config,
private IAvailabilityCoordinator $coordinator,
- private IUserManager $userManager) {
+ private IUserManager $userManager,
+ ) {
parent::__construct($timeFactory);
- // Interval 0 might look weird, but the last_checked is always moved
- // to the next time we need this and then it's 0 seconds ago.
+ // interval = 0 might look odd, but it's intentional. last_run is set to
+ // the user's next available time, so the job runs immediately when
+ // that time comes.
$this->setInterval(0);
}
@@ -70,14 +58,14 @@ class UserStatusAutomation extends TimedJob {
$userId = $argument['userId'];
$user = $this->userManager->get($userId);
- if($user === null) {
+ if ($user === null) {
return;
}
$ooo = $this->coordinator->getCurrentOutOfOfficeData($user);
$continue = $this->processOutOfOfficeData($user, $ooo);
- if($continue === false) {
+ if ($continue === false) {
return;
}
@@ -211,20 +199,18 @@ class UserStatusAutomation extends TimedJob {
return;
}
- if(!$hasDndForOfficeHours) {
+ if (!$hasDndForOfficeHours) {
// Office hours are not set to DND, so there is nothing to do.
return;
}
- $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND');
- // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists.
- $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY);
+ $this->logger->debug('User is currently NOT available, reverting call and meeting status if applicable and then setting DND');
$this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
$this->logger->debug('User status automation ran');
}
private function processOutOfOfficeData(IUser $user, ?IOutOfOfficeData $ooo): bool {
- if(empty($ooo)) {
+ if (empty($ooo)) {
// Reset the user status if the absence doesn't exist
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
@@ -232,12 +218,12 @@ class UserStatusAutomation extends TimedJob {
return true;
}
- if(!$this->coordinator->isInEffect($ooo)) {
+ if (!$this->coordinator->isInEffect($ooo)) {
// Reset the user status if the absence is (no longer) in effect
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
- if($ooo->getStartDate() > $this->time->getTime()) {
+ if ($ooo->getStartDate() > $this->time->getTime()) {
// Set the next run to take place at the start of the ooo period if it is in the future
// This might be overwritten if there is an availability setting, but we can't determine
// if this is the case here
@@ -247,10 +233,8 @@ class UserStatusAutomation extends TimedJob {
}
$this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status');
- // Revert both a possible 'CALL - away' and 'office hours - DND' status
- $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_CALL, IUserStatus::DND);
- $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
$this->manager->setUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage());
+
// Run at the end of an ooo period to return to availability / regular user status
// If it's overwritten by a custom status in the meantime, there's nothing we can do about it
$this->setLastRunToNextToggleTime($user->getUID(), $ooo->getEndDate());
diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
index 9890a615f93..d4faf3764e1 100644
--- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
+++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\BulkUpload;
@@ -34,15 +18,10 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BulkUploadPlugin extends ServerPlugin {
- private Folder $userFolder;
- private LoggerInterface $logger;
-
public function __construct(
- Folder $userFolder,
- LoggerInterface $logger
+ private Folder $userFolder,
+ private LoggerInterface $logger,
) {
- $this->userFolder = $userFolder;
- $this->logger = $logger;
}
/**
@@ -61,7 +40,7 @@ class BulkUploadPlugin extends ServerPlugin {
*/
public function httpPost(RequestInterface $request, ResponseInterface $response): bool {
// Limit bulk upload to the /dav/bulk endpoint
- if ($request->getPath() !== "bulk") {
+ if ($request->getPath() !== 'bulk') {
return true;
}
@@ -94,16 +73,16 @@ class BulkUploadPlugin extends ServerPlugin {
$node = $this->userFolder->getFirstNodeById($node->getId());
$writtenFiles[$headers['x-file-path']] = [
- "error" => false,
- "etag" => $node->getETag(),
- "fileid" => DavUtil::getDavFileId($node->getId()),
- "permissions" => DavUtil::getDavPermissions($node),
+ 'error' => false,
+ 'etag' => $node->getETag(),
+ 'fileid' => DavUtil::getDavFileId($node->getId()),
+ 'permissions' => DavUtil::getDavPermissions($node),
];
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['path' => $headers['x-file-path']]);
$writtenFiles[$headers['x-file-path']] = [
- "error" => true,
- "message" => $e->getMessage(),
+ 'error' => true,
+ 'message' => $e->getMessage(),
];
}
}
diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
index 7c977b42ccb..50f8cff76ba 100644
--- a/apps/dav/lib/BulkUpload/MultipartRequestParser.php
+++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\BulkUpload;
@@ -35,10 +20,10 @@ class MultipartRequestParser {
private $stream;
/** @var string */
- private $boundary = "";
+ private $boundary = '';
/** @var string */
- private $lastBoundary = "";
+ private $lastBoundary = '';
/**
* @throws BadRequest
@@ -55,14 +40,14 @@ class MultipartRequestParser {
}
if ($contentType === null) {
- throw new BadRequest("Content-Type can not be null");
+ throw new BadRequest('Content-Type can not be null');
}
$this->stream = $stream;
$boundary = $this->parseBoundaryFromHeaders($contentType);
- $this->boundary = '--'.$boundary."\r\n";
- $this->lastBoundary = '--'.$boundary."--\r\n";
+ $this->boundary = '--' . $boundary . "\r\n";
+ $this->lastBoundary = '--' . $boundary . "--\r\n";
}
/**
@@ -73,10 +58,16 @@ class MultipartRequestParser {
*/
private function parseBoundaryFromHeaders(string $contentType): string {
try {
+ if (!str_contains($contentType, ';')) {
+ throw new \InvalidArgumentException('No semicolon in header');
+ }
[$mimeType, $boundary] = explode(';', $contentType);
+ if (!str_contains($boundary, '=')) {
+ throw new \InvalidArgumentException('No equal in boundary header');
+ }
[$boundaryKey, $boundaryValue] = explode('=', $boundary);
} catch (\Exception $e) {
- throw new BadRequest("Error while parsing boundary in Content-Type header.", Http::STATUS_BAD_REQUEST, $e);
+ throw new BadRequest('Error while parsing boundary in Content-Type header.', Http::STATUS_BAD_REQUEST, $e);
}
$boundaryValue = trim($boundaryValue);
@@ -112,7 +103,7 @@ class MultipartRequestParser {
$seekBackResult = fseek($this->stream, -$expectedContentLength, SEEK_CUR);
if ($seekBackResult === -1) {
- throw new Exception("Unknown error while seeking content", Http::STATUS_INTERNAL_SERVER_ERROR);
+ throw new Exception('Unknown error while seeking content', Http::STATUS_INTERNAL_SERVER_ERROR);
}
return $expectedContent === $content;
@@ -150,7 +141,10 @@ class MultipartRequestParser {
$headers = $this->readPartHeaders();
- $content = $this->readPartContent($headers["content-length"], $headers["x-file-md5"]);
+ $length = (int)$headers['content-length'];
+
+ $this->validateHash($length, $headers['x-file-md5'] ?? '', $headers['oc-checksum'] ?? '');
+ $content = $this->readPartContent($length);
return [$headers, $content];
}
@@ -162,7 +156,7 @@ class MultipartRequestParser {
*/
private function readBoundary(): string {
if (!$this->isAtBoundary()) {
- throw new BadRequest("Boundary not found where it should be.");
+ throw new BadRequest('Boundary not found where it should be.');
}
return fread($this->stream, strlen($this->boundary));
@@ -196,12 +190,13 @@ class MultipartRequestParser {
}
}
- if (!isset($headers["content-length"])) {
- throw new LengthRequired("The Content-Length header must not be null.");
+ if (!isset($headers['content-length'])) {
+ throw new LengthRequired('The Content-Length header must not be null.');
}
- if (!isset($headers["x-file-md5"])) {
- throw new BadRequest("The X-File-MD5 header must not be null.");
+ // TODO: Drop $md5 condition when the latest desktop client that uses it is no longer supported.
+ if (!isset($headers['x-file-md5']) && !isset($headers['oc-checksum'])) {
+ throw new BadRequest('The hash headers must not be null.');
}
return $headers;
@@ -213,13 +208,7 @@ class MultipartRequestParser {
* @throws Exception
* @throws BadRequest
*/
- private function readPartContent(int $length, string $md5): string {
- $computedMd5 = $this->computeMd5Hash($length);
-
- if ($md5 !== $computedMd5) {
- throw new BadRequest("Computed md5 hash is incorrect.");
- }
-
+ private function readPartContent(int $length): string {
if ($length === 0) {
$content = '';
} else {
@@ -231,7 +220,7 @@ class MultipartRequestParser {
}
if ($length !== 0 && feof($this->stream)) {
- throw new Exception("Unexpected EOF while reading stream.");
+ throw new Exception('Unexpected EOF while reading stream.');
}
// Read '\r\n'.
@@ -241,12 +230,25 @@ class MultipartRequestParser {
}
/**
- * Compute the MD5 hash of the next x bytes.
+ * Compute the MD5 or checksum hash of the next x bytes.
+ * TODO: Drop $md5 argument when the latest desktop client that uses it is no longer supported.
*/
- private function computeMd5Hash(int $length): string {
- $context = hash_init('md5');
+ private function validateHash(int $length, string $fileMd5Header, string $checksumHeader): void {
+ if ($checksumHeader !== '') {
+ [$algorithm, $hash] = explode(':', $checksumHeader, 2);
+ } elseif ($fileMd5Header !== '') {
+ $algorithm = 'md5';
+ $hash = $fileMd5Header;
+ } else {
+ throw new BadRequest('No hash provided.');
+ }
+
+ $context = hash_init($algorithm);
hash_update_stream($context, $this->stream, $length);
fseek($this->stream, -$length, SEEK_CUR);
- return hash_final($context);
+ $computedHash = hash_final($context);
+ if ($hash !== $computedHash) {
+ throw new BadRequest("Computed $algorithm hash is incorrect ($computedHash).");
+ }
}
}
diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php
index 661a3dcc9ae..f0c49e6e28c 100644
--- a/apps/dav/lib/CalDAV/Activity/Backend.php
+++ b/apps/dav/lib/CalDAV/Activity/Backend.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity;
@@ -45,27 +26,13 @@ use Sabre\VObject\Reader;
*/
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- /** @var IUserManager */
- protected $userManager;
-
- public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager, IUserManager $userManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
- $this->userManager = $userManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -153,7 +120,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
@@ -181,7 +148,7 @@ class Backend {
[
'actor' => $currentUser,
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -212,7 +179,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
@@ -237,7 +204,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -266,7 +233,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -308,7 +275,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -335,7 +302,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -413,7 +380,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -463,7 +430,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
@@ -480,7 +447,7 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -554,7 +521,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $targetCalendarData['id'])
+ ->setObject('calendar', (int)$targetCalendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
@@ -571,12 +538,12 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'sourceCalendar' => [
- 'id' => (int) $sourceCalendarData['id'],
+ 'id' => (int)$sourceCalendarData['id'],
'uri' => $sourceCalendarData['uri'],
'name' => $sourceCalendarData['{DAV:}displayname'],
],
'targetCalendar' => [
- 'id' => (int) $targetCalendarData['id'],
+ 'id' => (int)$targetCalendarData['id'],
'uri' => $targetCalendarData['uri'],
'name' => $targetCalendarData['{DAV:}displayname'],
],
@@ -623,9 +590,9 @@ class Backend {
}
if ($componentType === 'VEVENT') {
- return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event'];
+ return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'event'];
}
- return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS];
+ return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'todo', 'status' => (string)$component->STATUS];
}
/**
diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
index 06258e3cf74..78579ee84b7 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
@@ -29,15 +12,10 @@ use OCP\IURLGenerator;
class Calendar implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -58,8 +36,8 @@ class Calendar implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
index 996250075c3..b001f90c28d 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
@@ -28,15 +12,10 @@ use OCP\IURLGenerator;
class Todo implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -57,8 +36,8 @@ class Todo implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
index 841011574d0..558abe0ca1a 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
@@ -33,27 +16,19 @@ use OCP\IURLGenerator;
use OCP\IUserManager;
abstract class Base implements IProvider {
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
/** @var string[] */
protected $groupDisplayNames = [];
- /** @var IURLGenerator */
- protected $url;
-
/**
* @param IUserManager $userManager
* @param IGroupManager $groupManager
- * @param IURLGenerator $urlGenerator
+ * @param IURLGenerator $url
*/
- public function __construct(IUserManager $userManager, IGroupManager $groupManager, IURLGenerator $urlGenerator) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->url = $urlGenerator;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ protected IURLGenerator $url,
+ ) {
}
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
@@ -66,18 +41,18 @@ abstract class Base implements IProvider {
* @return array
*/
protected function generateCalendarParameter($data, IL10N $l) {
- if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI &&
- $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
+ if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI
+ && $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $data['name'],
];
}
@@ -90,7 +65,7 @@ abstract class Base implements IProvider {
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => $name,
];
}
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
index e4cc5fadf7c..8c93ddae431 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
@@ -1,31 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -48,18 +29,9 @@ class Calendar extends Base {
public const SUBJECT_UNSHARE_USER = 'calendar_user_unshare';
public const SUBJECT_UNSHARE_GROUP = 'calendar_group_unshare';
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
@@ -68,11 +40,15 @@ class Calendar extends Base {
* @param IGroupManager $groupManager
* @param IEventMerger $eventMerger
*/
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger) {
+ public function __construct(
+ protected IFactory $languageFactory,
+ IURLGenerator $url,
+ protected IManager $activityManager,
+ IUserManager $userManager,
+ IGroupManager $groupManager,
+ protected IEventMerger $eventMerger,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
}
/**
@@ -80,12 +56,12 @@ class Calendar extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -143,7 +119,7 @@ class Calendar extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') {
$subject = $this->l->t('{actor} unshared calendar {calendar} from group {group}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
index 4dcb18fdb90..87551d7840b 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
@@ -1,32 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
-use OC_App;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -45,21 +25,9 @@ class Event extends Base {
public const SUBJECT_OBJECT_RESTORE = 'object_restore';
public const SUBJECT_OBJECT_DELETE = 'object_delete';
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- /** @var IAppManager */
- protected $appManager;
-
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
@@ -69,19 +37,23 @@ class Event extends Base {
* @param IEventMerger $eventMerger
* @param IAppManager $appManager
*/
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger, IAppManager $appManager) {
+ public function __construct(
+ protected IFactory $languageFactory,
+ IURLGenerator $url,
+ protected IManager $activityManager,
+ IUserManager $userManager,
+ IGroupManager $groupManager,
+ protected IEventMerger $eventMerger,
+ protected IAppManager $appManager,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
- $this->appManager = $appManager;
}
/**
* @param array $eventData
* @return array
*/
- protected function generateObjectParameter(array $eventData) {
+ protected function generateObjectParameter(array $eventData, string $affectedUser): array {
if (!isset($eventData['id']) || !isset($eventData['name'])) {
throw new \InvalidArgumentException();
}
@@ -95,17 +67,21 @@ class Event extends Base {
if (isset($eventData['link']) && is_array($eventData['link']) && $this->appManager->isEnabledForUser('calendar')) {
try {
// The calendar app needs to be manually loaded for the routes to be loaded
- OC_App::loadApp('calendar');
+ $this->appManager->loadApp('calendar');
$linkData = $eventData['link'];
- $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $linkData['calendar_uri'] . '/' . $linkData['object_uri']);
- $link = [
- 'view' => 'dayGridMonth',
- 'timeRange' => 'now',
- 'mode' => 'sidebar',
+ $calendarUri = $this->urlencodeLowerHex($linkData['calendar_uri']);
+ if ($affectedUser === $linkData['owner']) {
+ $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $calendarUri . '/' . $linkData['object_uri']);
+ } else {
+ // Can't use the "real" owner and calendar names here because we create a custom
+ // calendar for incoming shares with the name "<calendar>_shared_by_<sharer>".
+ // Hack: Fix the link by generating it for the incoming shared calendar instead,
+ // as seen from the affected user.
+ $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $affectedUser . '/' . $calendarUri . '_shared_by_' . $linkData['owner'] . '/' . $linkData['object_uri']);
+ }
+ $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexdirect.edit', [
'objectId' => $objectId,
- 'recurrenceId' => 'next'
- ];
- $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexview.timerange.edit', $link);
+ ]);
} catch (\Exception $error) {
// Do nothing
}
@@ -118,12 +94,12 @@ class Event extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_event') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -159,7 +135,7 @@ class Event extends Base {
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') {
$subject = $this->l->t('You restored event {event} of calendar {calendar}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
@@ -189,7 +165,7 @@ class Event extends Base {
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
@@ -198,7 +174,7 @@ class Event extends Base {
case self::SUBJECT_OBJECT_RESTORE . '_event_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -210,13 +186,13 @@ class Event extends Base {
'actor' => $this->generateUserParameter($parameters['actor']),
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_MOVE . '_event_self':
return [
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -233,25 +209,37 @@ class Event extends Base {
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'event' => $this->generateObjectParameter($parameters[2]),
+ 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
case self::SUBJECT_OBJECT_UPDATE . '_event_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'event' => $this->generateObjectParameter($parameters[2]),
+ 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
}
throw new \InvalidArgumentException();
}
- private function generateClassifiedObjectParameter(array $eventData) {
- $parameter = $this->generateObjectParameter($eventData);
+ private function generateClassifiedObjectParameter(array $eventData, string $affectedUser): array {
+ $parameter = $this->generateObjectParameter($eventData, $affectedUser);
if (!empty($eventData['classified'])) {
$parameter['name'] = $this->l->t('Busy');
}
return $parameter;
}
+
+ /**
+ * Return urlencoded string but with lower cased hex sequences.
+ * The remaining casing will be untouched.
+ */
+ private function urlencodeLowerHex(string $raw): string {
+ return preg_replace_callback(
+ '/%[0-9A-F]{2}/',
+ static fn (array $matches) => strtolower($matches[0]),
+ urlencode($raw),
+ );
+ }
}
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
index 0a8434b9595..fc0625ec970 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
@@ -1,29 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
class Todo extends Event {
@@ -33,12 +16,12 @@ class Todo extends Event {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_todo') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -74,7 +57,7 @@ class Todo extends Event {
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') {
$subject = $this->l->t('You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
@@ -104,7 +87,7 @@ class Todo extends Event {
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
@@ -113,7 +96,7 @@ class Todo extends Event {
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -125,13 +108,13 @@ class Todo extends Event {
'actor' => $this->generateUserParameter($parameters['actor']),
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_MOVE . '_todo_self':
return [
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -150,7 +133,7 @@ class Todo extends Event {
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'todo' => $this->generateObjectParameter($parameters[2]),
+ 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
@@ -159,7 +142,7 @@ class Todo extends Event {
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'todo' => $this->generateObjectParameter($parameters[2]),
+ 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
}
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
index 20325a253f4..7ab7f16dbbb 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -30,14 +12,12 @@ use OCP\Activity\ActivitySettings;
use OCP\IL10N;
abstract class CalDAVSetting extends ActivitySettings {
- /** @var IL10N */
- protected $l;
-
/**
* @param IL10N $l
*/
- public function __construct(IL10N $l) {
- $this->l = $l;
+ public function __construct(
+ protected IL10N $l,
+ ) {
}
public function getGroupIdentifier() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
index 4a226fca439..0ad86a919bc 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -42,8 +25,8 @@ class Calendar extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Event.php b/apps/dav/lib/CalDAV/Activity/Setting/Event.php
index 0239296a403..ea9476d6f08 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Event.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -42,8 +25,8 @@ class Event extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
index a4ac3f5d569..ed8377b0ffa 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -43,8 +26,8 @@ class Todo extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
index 68b71404371..87d26324c32 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\AppCalendar;
@@ -41,12 +24,14 @@ use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Reader;
class AppCalendar extends ExternalCalendar {
- protected string $principal;
protected ICalendar $calendar;
- public function __construct(string $appId, ICalendar $calendar, string $principal) {
+ public function __construct(
+ string $appId,
+ ICalendar $calendar,
+ protected string $principal,
+ ) {
parent::__construct($appId, $calendar->getUri());
- $this->principal = $principal;
$this->calendar = $calendar;
}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
index ddf76e27f3a..72f2ed2c163 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
@@ -3,29 +3,14 @@
declare(strict_types=1);
/**
- * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\AppCalendar;
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CalendarImpl;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCP\Calendar\IManager;
@@ -33,12 +18,10 @@ use Psr\Log\LoggerInterface;
/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
class AppCalendarPlugin implements ICalendarProvider {
- protected IManager $manager;
- protected LoggerInterface $logger;
-
- public function __construct(IManager $manager, LoggerInterface $logger) {
- $this->manager = $manager;
- $this->logger = $logger;
+ public function __construct(
+ protected IManager $manager,
+ protected LoggerInterface $logger,
+ ) {
}
public function getAppID(): string {
@@ -68,7 +51,7 @@ class AppCalendarPlugin implements ICalendarProvider {
return array_values(
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
// We must not provide a wrapper for DAV calendars
- return ! ($c instanceof \OCA\DAV\CalDAV\CalendarImpl);
+ return ! (($c instanceof CalendarImpl) || ($c instanceof CachedSubscriptionImpl));
})
);
}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
index bfcab35f74e..3c62a26df54 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\AppCalendar;
@@ -37,14 +20,11 @@ use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Property\ICalendar\DateTime;
class CalendarObject implements ICalendarObject, IACL {
- private VCalendar $vobject;
- private AppCalendar $calendar;
- private ICalendar|ICreateFromString $backend;
-
- public function __construct(AppCalendar $calendar, ICalendar $backend, VCalendar $vobject) {
- $this->backend = $backend;
- $this->calendar = $calendar;
- $this->vobject = $vobject;
+ public function __construct(
+ private AppCalendar $calendar,
+ private ICalendar|ICreateFromString $backend,
+ private VCalendar $vobject,
+ ) {
}
public function getOwner() {
diff --git a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
index 89e50c7da6b..71b9acb939b 100644
--- a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
+++ b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * CalDAV App
- *
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Auth;
diff --git a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
index 96669558818..ed89638451e 100644
--- a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
+++ b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * CalDAV App
- *
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Auth;
diff --git a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
index f7d68e4ec1d..681709cdb6f 100644
--- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
+++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
@@ -1,31 +1,14 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\BirthdayCalendar;
use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CalDAV\CalendarHome;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IUser;
use Sabre\DAV\Server;
@@ -43,23 +26,10 @@ class EnablePlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var BirthdayService
- */
- protected $birthdayService;
-
- /**
* @var Server
*/
protected $server;
- /** @var IUser */
- private $user;
-
/**
* PublishPlugin constructor.
*
@@ -67,10 +37,11 @@ class EnablePlugin extends ServerPlugin {
* @param BirthdayService $birthdayService
* @param IUser $user
*/
- public function __construct(IConfig $config, BirthdayService $birthdayService, IUser $user) {
- $this->config = $config;
- $this->birthdayService = $birthdayService;
- $this->user = $user;
+ public function __construct(
+ protected IConfig $config,
+ protected BirthdayService $birthdayService,
+ private IUser $user,
+ ) {
}
/**
@@ -123,26 +94,26 @@ class EnablePlugin extends ServerPlugin {
*/
public function httpPost(RequestInterface $request, ResponseInterface $response) {
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
- if (!($node instanceof CalendarHome)) {
+ if (!$node instanceof CalendarHome) {
return;
}
$requestBody = $request->getBodyAsString();
$this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
- if ($documentType !== '{'.self::NS_Nextcloud.'}enable-birthday-calendar') {
+ if ($documentType !== '{' . self::NS_Nextcloud . '}enable-birthday-calendar') {
return;
}
$owner = substr($node->getOwner(), 17);
- if($owner !== $this->user->getUID()) {
- $this->server->httpResponse->setStatus(403);
+ if ($owner !== $this->user->getUID()) {
+ $this->server->httpResponse->setStatus(Http::STATUS_FORBIDDEN);
return false;
}
$this->config->setUserValue($this->user->getUID(), 'dav', 'generateBirthdayCalendar', 'yes');
$this->birthdayService->syncUser($this->user->getUID());
- $this->server->httpResponse->setStatus(204);
+ $this->server->httpResponse->setStatus(Http::STATUS_NO_CONTENT);
return false;
}
diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php
index 93344e33186..680b228766f 100644
--- a/apps/dav/lib/CalDAV/BirthdayService.php
+++ b/apps/dav/lib/CalDAV/BirthdayService.php
@@ -3,33 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Achim Königs <garfonso@tratschtante.de>
- * @author Christian Weiske <cweiske@cweiske.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Sven Strickroth <email@cs-ware.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Valdnet <47037905+Valdnet@users.noreply.github.com>
- * @author Cédric Neukom <github@webguy.ch>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -56,28 +32,17 @@ class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
- private GroupPrincipalBackend $principalBackend;
- private CalDavBackend $calDavBackEnd;
- private CardDavBackend $cardDavBackEnd;
- private IConfig $config;
- private IDBConnection $dbConnection;
- private IL10N $l10n;
-
/**
* BirthdayService constructor.
*/
- public function __construct(CalDavBackend $calDavBackEnd,
- CardDavBackend $cardDavBackEnd,
- GroupPrincipalBackend $principalBackend,
- IConfig $config,
- IDBConnection $dbConnection,
- IL10N $l10n) {
- $this->calDavBackEnd = $calDavBackEnd;
- $this->cardDavBackEnd = $cardDavBackEnd;
- $this->principalBackend = $principalBackend;
- $this->config = $config;
- $this->dbConnection = $dbConnection;
- $this->l10n = $l10n;
+ public function __construct(
+ private CalDavBackend $calDavBackEnd,
+ private CardDavBackend $cardDavBackEnd,
+ private GroupPrincipalBackend $principalBackend,
+ private IConfig $config,
+ private IDBConnection $dbConnection,
+ private IL10N $l10n,
+ ) {
}
public function onCardChanged(int $addressBookId,
@@ -111,7 +76,7 @@ class BirthdayService {
return;
}
foreach ($datesToSync as $type) {
- $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset);
+ $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
}
}
}
@@ -132,7 +97,7 @@ class BirthdayService {
$calendar = $this->ensureCalendarExists($principalUri);
foreach (['', '-death', '-anniversary'] as $tag) {
- $objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
+ $objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
@@ -163,9 +128,9 @@ class BirthdayService {
* @return VCalendar|null
* @throws InvalidDataException
*/
- public function buildDateFromContact(string $cardData,
- string $dateField,
- string $postfix,
+ public function buildDateFromContact(string $cardData,
+ string $dateField,
+ string $postfix,
?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
@@ -271,7 +236,7 @@ class BirthdayService {
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
if ($originalYear !== null) {
- $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
+ $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear;
}
if ($reminderOffset) {
$alarm = $vCal->createComponent('VALARM');
@@ -288,7 +253,7 @@ class BirthdayService {
* @param string $user
*/
public function resetForUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!$calendar) {
return; // The user's birthday calendar doesn't exist, no need to purge it
@@ -305,13 +270,13 @@ class BirthdayService {
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function syncUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$this->ensureCalendarExists($principal);
$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
foreach ($books as $book) {
$cards = $this->cardDavBackEnd->getCards($book['id']);
foreach ($cards as $card) {
- $this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
+ $this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']);
}
}
}
@@ -330,8 +295,8 @@ class BirthdayService {
}
return (
- $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
- $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
+ $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue()
+ || $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
);
}
diff --git a/apps/dav/lib/CalDAV/CachedSubscription.php b/apps/dav/lib/CalDAV/CachedSubscription.php
index 4047f8dad0b..75ee5cb440f 100644
--- a/apps/dav/lib/CalDAV/CachedSubscription.php
+++ b/apps/dav/lib/CalDAV/CachedSubscription.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -73,6 +54,11 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => '{DAV:}authenticated',
'protected' => true,
],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
];
}
@@ -97,7 +83,6 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
-
];
}
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
new file mode 100644
index 00000000000..cc1bab6d4fc
--- /dev/null
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV;
+
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICalendarIsEnabled;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
+use OCP\Constants;
+
+class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendarIsShared, ICalendarIsWritable {
+
+ public function __construct(
+ private CachedSubscription $calendar,
+ /** @var array<string, mixed> */
+ private array $calendarInfo,
+ private CalDavBackend $backend,
+ ) {
+ }
+
+ /**
+ * @return string defining the technical unique key
+ * @since 13.0.0
+ */
+ public function getKey(): string {
+ return (string)$this->calendarInfo['id'];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getUri(): string {
+ return $this->calendarInfo['uri'];
+ }
+
+ /**
+ * In comparison to getKey() this function returns a human readable (maybe translated) name
+ * @since 13.0.0
+ */
+ public function getDisplayName(): ?string {
+ return $this->calendarInfo['{DAV:}displayname'];
+ }
+
+ /**
+ * Calendar color
+ * @since 13.0.0
+ */
+ public function getDisplayColor(): ?string {
+ return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
+ }
+
+ public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
+ return $this->backend->search($this->calendarInfo, $pattern, $searchProperties, $options, $limit, $offset);
+ }
+
+ /**
+ * @return int build up using \OCP\Constants
+ * @since 13.0.0
+ */
+ public function getPermissions(): int {
+ $permissions = $this->calendar->getACL();
+ $result = 0;
+ foreach ($permissions as $permission) {
+ switch ($permission['privilege']) {
+ case '{DAV:}read':
+ $result |= Constants::PERMISSION_READ;
+ break;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @since 32.0.0
+ */
+ public function isEnabled(): bool {
+ return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
+ }
+
+ public function isWritable(): bool {
+ return false;
+ }
+
+ public function isDeleted(): bool {
+ return false;
+ }
+
+ public function isShared(): bool {
+ return true;
+ }
+
+ public function getSource(): string {
+ return $this->calendarInfo['source'];
+ }
+}
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionObject.php b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
index 3c1373763e1..dc9141a61b8 100644
--- a/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
new file mode 100644
index 00000000000..d64f039d05b
--- /dev/null
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV;
+
+use OCP\Calendar\ICalendarProvider;
+
+class CachedSubscriptionProvider implements ICalendarProvider {
+
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ ) {
+ }
+
+ public function getCalendars(string $principalUri, array $calendarUris = []): array {
+ $calendarInfos = $this->calDavBackend->getSubscriptionsForUser($principalUri);
+
+ if (count($calendarUris) > 0) {
+ $calendarInfos = array_filter($calendarInfos, fn (array $subscription) => in_array($subscription['uri'], $calendarUris));
+ }
+
+ $calendarInfos = array_values(array_filter($calendarInfos));
+
+ $iCalendars = [];
+ foreach ($calendarInfos as $calendarInfo) {
+ $calendar = new CachedSubscription($this->calDavBackend, $calendarInfo);
+ $iCalendars[] = new CachedSubscriptionImpl(
+ $calendar,
+ $calendarInfo,
+ $this->calDavBackend,
+ );
+ }
+ return $iCalendars;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index 1424ee4f9be..d5b0d875ede 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -1,46 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018 Georg Ehrke
- * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>)
- *
- * @author Chih-Hsuan Yen <yan12125@gmail.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author dartcafe <github@dartcafe.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author leith abdulla <online-nextcloud@eleith.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Simon Spannagel <simonspa@kth.se>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
use DateTime;
+use DateTimeImmutable;
use DateTimeInterface;
+use Generator;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\Connector\Sabre\Principal;
@@ -51,12 +21,6 @@ use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectDeletedEvent;
-use OCA\DAV\Events\CalendarObjectMovedEvent;
-use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectRestoredEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarPublishedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
@@ -66,6 +30,13 @@ use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Events\SubscriptionUpdatedEvent;
use OCP\AppFramework\Db\TTransactional;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent;
+use OCP\Calendar\Events\CalendarObjectRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -97,6 +68,8 @@ use Sabre\VObject\ParseException;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
+use Sabre\VObject\Recur\NoInstancesException;
use function array_column;
use function array_map;
use function array_merge;
@@ -118,6 +91,19 @@ use function time;
* Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
*
* @package OCA\DAV\CalDAV
+ *
+ * @psalm-type CalendarInfo = array{
+ * id: int,
+ * uri: string,
+ * principaluri: string,
+ * '{http://calendarserver.org/ns/}getctag': string,
+ * '{http://sabredav.org/ns}sync-token': int,
+ * '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet,
+ * '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp,
+ * '{DAV:}displayname': string,
+ * '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string,
+ * '{http://nextcloud.com/ns}owner-displayname': string,
+ * }
*/
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
use TTransactional;
@@ -208,7 +194,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
protected array $userDisplayNames;
+ private string $dbObjectsTable = 'calendarobjects';
private string $dbObjectPropertiesTable = 'calendarobjects_props';
+ private string $dbObjectInvitationsTable = 'calendar_invitations';
private array $cachedObjects = [];
public function __construct(
@@ -225,15 +213,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * Return the number of calendars for a principal
+ * Return the number of calendars owned by the given principal.
*
- * By default this excludes the automatically generated birthday calendar
+ * Calendars shared with the given principal are not counted!
*
- * @param $principalUri
- * @param bool $excludeBirthday
- * @return int
+ * By default, this excludes the automatically generated birthday calendar.
*/
- public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
+ public function getCalendarsForUserCount(string $principalUri, bool $excludeBirthday = true): int {
$principalUri = $this->convertPrincipal($principalUri, true);
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('*'))
@@ -289,8 +275,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendars = [];
while (($row = $result->fetch()) !== false) {
$calendars[] = [
- 'id' => (int) $row['id'],
- 'deleted_at' => (int) $row['deleted_at'],
+ 'id' => (int)$row['id'],
+ 'deleted_at' => (int)$row['deleted_at'],
];
}
$result->closeCursor();
@@ -350,7 +336,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendars = [];
while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -360,8 +346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
@@ -384,7 +370,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$fields = array_column($this->propertyMap, 0);
$fields = array_map(function (string $field) {
- return 'a.'.$field;
+ return 'a.' . $field;
}, $fields);
$fields[] = 'a.id';
$fields[] = 'a.uri';
@@ -413,19 +399,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
while ($row = $results->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
if ($row['principaluri'] === $principalUri) {
continue;
}
- $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
+ $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
if (isset($calendars[$row['id']])) {
if ($readOnly) {
// New share can not have more permissions than the old one.
continue;
}
- if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
- $calendars[$row['id']][$readOnlyPropertyName] === 0) {
+ if (isset($calendars[$row['id']][$readOnlyPropertyName])
+ && $calendars[$row['id']][$readOnlyPropertyName] === 0) {
// Old share is already read-write, no more permissions can be gained
continue;
}
@@ -442,8 +428,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $uri,
'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
@@ -483,7 +469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$calendars = [];
while ($row = $stmt->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -492,8 +478,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
];
@@ -533,7 +519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->executeQuery();
while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
[, $name] = Uri\split($row['principaluri']);
$row['displayname'] = $row['displayname'] . "($name)";
$components = [];
@@ -544,8 +530,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['publicuri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
@@ -598,7 +584,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
throw new NotFound('Node with name \'' . $uri . '\' could not be found');
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
[, $name] = Uri\split($row['principaluri']);
$row['displayname'] = $row['displayname'] . ' ' . "($name)";
$components = [];
@@ -609,8 +595,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['publicuri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
@@ -653,7 +639,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -663,8 +649,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
];
@@ -677,7 +663,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
+ * @psalm-return CalendarInfo|null
+ * @return array|null
*/
public function getCalendarById(int $calendarId): ?array {
$fields = array_column($this->propertyMap, 0);
@@ -701,7 +688,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -711,7 +698,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
@@ -749,7 +736,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$subscription = [
'id' => $row['id'],
'uri' => $row['uri'],
@@ -757,7 +744,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'source' => $row['source'],
'lastmodified' => $row['lastmodified'],
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
+ ];
+
+ return $this->rowToSubscription($row, $subscription);
+ }
+
+ public function getSubscriptionByUri(string $principal, string $uri): ?array {
+ $fields = array_column($this->subscriptionPropertyMap, 0);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'source';
+ $fields[] = 'synctoken';
+ $fields[] = 'principaluri';
+ $fields[] = 'lastmodified';
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)
+ ->from('calendarsubscriptions')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
+ ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
+ ->setMaxResults(1);
+ $stmt = $query->executeQuery();
+
+ $row = $stmt->fetch();
+ $stmt->closeCursor();
+ if ($row === false) {
+ return null;
+ }
+
+ $row['principaluri'] = (string)$row['principaluri'];
+ $subscription = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ 'source' => $row['source'],
+ 'lastmodified' => $row['lastmodified'],
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
];
return $this->rowToSubscription($row, $subscription);
@@ -786,7 +810,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'uri' => $calendarUri,
'synctoken' => 1,
'transparent' => 0,
- 'components' => 'VEVENT,VTODO',
+ 'components' => 'VEVENT,VTODO,VJOURNAL',
'displayname' => $calendarUri
];
@@ -805,7 +829,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
if (isset($properties[$transp])) {
- $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
+ $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
}
foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
@@ -858,7 +882,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
switch ($propertyName) {
case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
$fieldName = 'transparent';
- $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
+ $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
break;
default:
$fieldName = $this->propertyMap[$propertyName][0];
@@ -875,7 +899,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
$query->executeStatement();
- $this->addChanges($calendarId, [""], 2);
+ $this->addChanges($calendarId, [''], 2);
$calendarData = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
@@ -895,7 +919,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
- $this->atomic(function () use ($calendarId, $forceDeletePermanently) {
+ $this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
// The calendar is deleted right away if this is either enforced by the caller
// or the special contacts birthday calendar or when the preference of an empty
// retention (0 seconds) is set, which signals a disabled trashbin.
@@ -906,6 +930,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendarData = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
+ $this->purgeCalendarInvitations($calendarId);
+
$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
@@ -956,7 +982,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
public function restoreCalendar(int $id): void {
- $this->atomic(function () use ($id) {
+ $this->atomic(function () use ($id): void {
$qb = $this->db->getQueryBuilder();
$update = $qb->update('calendars')
->set('deleted_at', $qb->createNamedParameter(null))
@@ -977,6 +1003,81 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * Returns all calendar entries as a stream of data
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<array>
+ */
+ public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator {
+ // extract options
+ $rangeStart = $options?->getRangeStart();
+ $rangeCount = $options?->getRangeCount();
+ // construct query
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from('calendarobjects')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ if ($rangeStart !== null) {
+ $qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart)));
+ }
+ if ($rangeCount !== null) {
+ $qb->setMaxResults($rangeCount);
+ }
+ if ($rangeStart !== null || $rangeCount !== null) {
+ $qb->orderBy('uid', 'ASC');
+ }
+ $rs = $qb->executeQuery();
+ // iterate through results
+ try {
+ while (($row = $rs->fetch()) !== false) {
+ yield $row;
+ }
+ } finally {
+ $rs->closeCursor();
+ }
+ }
+
+ /**
+ * Returns all calendar objects with limited metadata for a calendar
+ *
+ * Every item contains an array with the following keys:
+ * * id - the table row id
+ * * etag - An arbitrary string
+ * * uri - a unique key which will be used to construct the uri. This can
+ * be any arbitrary string.
+ * * calendardata - The iCalendar-compatible calendar data
+ *
+ * @param mixed $calendarId
+ * @param int $calendarType
+ * @return array
+ */
+ public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id','uid', 'etag', 'uri', 'calendardata'])
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
+ ->andWhere($query->expr()->isNull('deleted_at'));
+ $stmt = $query->executeQuery();
+
+ $result = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $result[$row['uid']] = [
+ 'id' => $row['id'],
+ 'etag' => $row['etag'],
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ ];
+ }
+ $stmt->closeCursor();
+
+ return $result;
+ }
+
+ /**
* Delete all of an user's shares
*
* @param string $principaluri
@@ -1061,12 +1162,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'uri' => $row['uri'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
- 'calendarid' => (int) $row['calendarid'],
- 'calendartype' => (int) $row['calendartype'],
- 'size' => (int) $row['size'],
+ 'calendarid' => (int)$row['calendarid'],
+ 'calendartype' => (int)$row['calendartype'],
+ 'size' => (int)$row['size'],
'component' => strtolower($row['componenttype']),
- 'classification' => (int) $row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ 'classification' => (int)$row['classification'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
$stmt->closeCursor();
@@ -1105,7 +1206,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'size' => (int)$row['size'],
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
$stmt->closeCursor();
@@ -1136,7 +1237,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return $this->cachedObjects[$key];
}
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
+ $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
->from('calendarobjects')
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
@@ -1158,6 +1259,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return [
'id' => $row['id'],
'uri' => $row['uri'],
+ 'uid' => $row['uid'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
'calendarid' => $row['calendarid'],
@@ -1165,7 +1267,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
@@ -1254,7 +1356,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
->andWhere($qb->expr()->isNull('deleted_at'));
$result = $qb->executeQuery();
- $count = (int) $result->fetchOne();
+ $count = (int)$result->fetchOne();
$result->closeCursor();
if ($count !== 0) {
@@ -1342,15 +1444,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
$query = $this->db->getQueryBuilder();
$query->update('calendarobjects')
- ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
- ->set('lastmodified', $query->createNamedParameter(time()))
- ->set('etag', $query->createNamedParameter($extraData['etag']))
- ->set('size', $query->createNamedParameter($extraData['size']))
- ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
- ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
- ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
- ->set('classification', $query->createNamedParameter($extraData['classification']))
- ->set('uid', $query->createNamedParameter($extraData['uid']))
+ ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
+ ->set('lastmodified', $query->createNamedParameter(time()))
+ ->set('etag', $query->createNamedParameter($extraData['etag']))
+ ->set('size', $query->createNamedParameter($extraData['size']))
+ ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
+ ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
+ ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
+ ->set('classification', $query->createNamedParameter($extraData['classification']))
+ ->set('uid', $query->createNamedParameter($extraData['uid']))
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1380,37 +1482,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* Moves a calendar object from calendar to calendar.
*
- * @param int $sourceCalendarId
+ * @param string $sourcePrincipalUri
+ * @param int $sourceObjectId
+ * @param string $targetPrincipalUri
* @param int $targetCalendarId
- * @param int $objectId
- * @param string $oldPrincipalUri
- * @param string $newPrincipalUri
+ * @param string $tragetObjectUri
* @param int $calendarType
* @return bool
* @throws Exception
*/
- public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
+ public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
$this->cachedObjects = [];
- return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) {
- $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
+ return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
+ $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
if (empty($object)) {
return false;
}
+ $sourceCalendarId = $object['calendarid'];
+ $sourceObjectUri = $object['uri'];
+
$query = $this->db->getQueryBuilder();
$query->update('calendarobjects')
->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
+ ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->executeStatement();
- $this->purgeProperties($sourceCalendarId, $objectId);
- $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
+ $this->purgeProperties($sourceCalendarId, $sourceObjectId);
+ $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
- $this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType);
- $this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType);
+ $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
+ $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
- $object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
+ $object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId);
// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
if (empty($object)) {
return false;
@@ -1432,25 +1537,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $this->db);
}
-
- /**
- * @param int $calendarObjectId
- * @param int $classification
- */
- public function setClassification($calendarObjectId, $classification) {
- $this->cachedObjects = [];
- if (!in_array($classification, [
- self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
- ])) {
- throw new \InvalidArgumentException();
- }
- $query = $this->db->getQueryBuilder();
- $query->update('calendarobjects')
- ->set('classification', $query->createNamedParameter($classification))
- ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
- ->executeStatement();
- }
-
/**
* Deletes an existing calendar object.
*
@@ -1464,7 +1550,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) {
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
if ($data === null) {
@@ -1478,6 +1564,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->purgeProperties($calendarId, $data['id']);
+ $this->purgeObjectInvitations($data['uid']);
+
if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
$calendarRow = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
@@ -1493,13 +1581,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (!empty($pathInfo['extension'])) {
// Append a suffix to "free" the old URI for recreation
$newUri = sprintf(
- "%s-deleted.%s",
+ '%s-deleted.%s',
$pathInfo['filename'],
$pathInfo['extension']
);
} else {
$newUri = sprintf(
- "%s-deleted",
+ '%s-deleted',
$pathInfo['filename']
);
}
@@ -1546,9 +1634,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function restoreCalendarObject(array $objectData): void {
$this->cachedObjects = [];
- $this->atomic(function () use ($objectData) {
- $id = (int) $objectData['id'];
- $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
+ $this->atomic(function () use ($objectData): void {
+ $id = (int)$objectData['id'];
+ $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
$targetObject = $this->getCalendarObject(
$objectData['calendarid'],
$restoreUri
@@ -1577,17 +1665,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
// Welp, this should possibly not have happened, but let's ignore
return;
}
- $this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']);
+ $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
- $calendarRow = $this->getCalendarById((int) $row['calendarid']);
+ $calendarRow = $this->getCalendarById((int)$row['calendarid']);
if ($calendarRow === null) {
throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
}
$this->dispatcher->dispatchTyped(
new CalendarObjectRestoredEvent(
- (int) $objectData['calendarid'],
+ (int)$objectData['calendarid'],
$calendarRow,
- $this->getShares((int) $row['calendarid']),
+ $this->getShares((int)$row['calendarid']),
$row
)
);
@@ -1674,7 +1762,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
+ $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
->from('calendarobjects')
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1706,13 +1794,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
try {
$matches = $this->validateFilterForObject($row, $filters);
} catch (ParseException $ex) {
- $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
+ $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
'app' => 'dav',
'exception' => $ex,
]);
continue;
} catch (InvalidDataException $ex) {
- $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
+ $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
+ 'app' => 'dav',
+ 'exception' => $ex,
+ ]);
+ continue;
+ } catch (MaxInstancesExceededException $ex) {
+ $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
'app' => 'dav',
'exception' => $ex,
]);
@@ -1835,7 +1929,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($compExpr)
->andWhere($propParamExpr)
->andWhere($query->expr()->iLike('i.value',
- $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
+ $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
->andWhere($query->expr()->isNull('deleted_at'));
if ($offset) {
@@ -1877,17 +1971,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
array $searchProperties,
array $options,
$limit,
- $offset
+ $offset,
) {
$outerQuery = $this->db->getQueryBuilder();
$innerQuery = $this->db->getQueryBuilder();
+ if (isset($calendarInfo['source'])) {
+ $calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
+ } else {
+ $calendarType = self::CALENDAR_TYPE_CALENDAR;
+ }
+
$innerQuery->selectDistinct('op.objectid')
->from($this->dbObjectPropertiesTable, 'op')
->andWhere($innerQuery->expr()->eq('op.calendarid',
$outerQuery->createNamedParameter($calendarInfo['id'])))
->andWhere($innerQuery->expr()->eq('op.calendartype',
- $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ $outerQuery->createNamedParameter($calendarType)));
$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
->from('calendarobjects', 'c')
@@ -1900,29 +2000,48 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
if (!empty($searchProperties)) {
- $or = $innerQuery->expr()->orX();
+ $or = [];
foreach ($searchProperties as $searchProperty) {
- $or->add($innerQuery->expr()->eq('op.name',
- $outerQuery->createNamedParameter($searchProperty)));
+ $or[] = $innerQuery->expr()->eq('op.name',
+ $outerQuery->createNamedParameter($searchProperty));
}
- $innerQuery->andWhere($or);
+ $innerQuery->andWhere($innerQuery->expr()->orX(...$or));
}
if ($pattern !== '') {
$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
- $outerQuery->createNamedParameter('%' .
- $this->db->escapeLikeParameter($pattern) . '%')));
+ $outerQuery->createNamedParameter('%'
+ . $this->db->escapeLikeParameter($pattern) . '%')));
}
- if (isset($options['timerange'])) {
- if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
- $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
- }
- if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
- $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
- }
+ $start = null;
+ $end = null;
+
+ $hasLimit = is_int($limit);
+ $hasTimeRange = false;
+
+ if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $start */
+ $start = $options['timerange']['start'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->gt(
+ 'lastoccurence',
+ $outerQuery->createNamedParameter($start->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
+ }
+
+ if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $end */
+ $end = $options['timerange']['end'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->lt(
+ 'firstoccurence',
+ $outerQuery->createNamedParameter($end->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
}
if (isset($options['uid'])) {
@@ -1930,64 +2049,56 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
if (!empty($options['types'])) {
- $or = $outerQuery->expr()->orX();
+ $or = [];
foreach ($options['types'] as $type) {
- $or->add($outerQuery->expr()->eq('componenttype',
- $outerQuery->createNamedParameter($type)));
+ $or[] = $outerQuery->expr()->eq('componenttype',
+ $outerQuery->createNamedParameter($type));
}
- $outerQuery->andWhere($or);
+ $outerQuery->andWhere($outerQuery->expr()->orX(...$or));
}
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
- if ($offset) {
- $outerQuery->setFirstResult($offset);
- }
- if ($limit) {
- $outerQuery->setMaxResults($limit);
- }
+ // Without explicit order by its undefined in which order the SQL server returns the events.
+ // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
+ $outerQuery->addOrderBy('id');
- $result = $outerQuery->executeQuery();
- $calendarObjects = [];
- while (($row = $result->fetch()) !== false) {
- $start = $options['timerange']['start'] ?? null;
- $end = $options['timerange']['end'] ?? null;
+ $offset = (int)$offset;
+ $outerQuery->setFirstResult($offset);
- if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
- // No filter required
- $calendarObjects[] = $row;
- continue;
- }
+ $calendarObjects = [];
- $isValid = $this->validateFilterForObject($row, [
- 'name' => 'VCALENDAR',
- 'comp-filters' => [
- [
- 'name' => 'VEVENT',
- 'comp-filters' => [],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => [
- 'start' => $start,
- 'end' => $end,
- ],
- ],
- ],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => null,
- ]);
- if (is_resource($row['calendardata'])) {
- // Put the stream back to the beginning so it can be read another time
- rewind($row['calendardata']);
- }
- if ($isValid) {
- $calendarObjects[] = $row;
- }
+ if ($hasLimit && $hasTimeRange) {
+ /**
+ * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
+ *
+ * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
+ * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
+ *
+ * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
+ * and discard the events after evaluating the reoccurrence rules because they are not due within
+ * the next 14 days and end up with an empty result even if there are two events to show.
+ *
+ * The workaround for search requests with a limit and time range is asking for more row than requested
+ * and retrying if we have not reached the limit.
+ *
+ * 25 rows and 3 retries is entirely arbitrary.
+ */
+ $maxResults = (int)max($limit, 25);
+ $outerQuery->setMaxResults($maxResults);
+
+ for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
+ $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
+ $outerQuery->setFirstResult($offset += $maxResults);
+ }
+
+ $calendarObjects = array_slice($calendarObjects, 0, $limit, false);
+ } else {
+ $outerQuery->setMaxResults($limit);
+ $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
}
- $result->closeCursor();
- return array_map(function ($o) use ($options) {
+ $calendarObjects = array_map(function ($o) use ($options) {
$calendarData = Reader::read($o['calendardata']);
// Expand recurrences if an explicit time range is requested
@@ -2023,6 +2134,72 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $timezones),
];
}, $calendarObjects);
+
+ usort($calendarObjects, function (array $a, array $b) {
+ /** @var DateTimeImmutable $startA */
+ $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+ /** @var DateTimeImmutable $startB */
+ $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+
+ return $startA->getTimestamp() <=> $startB->getTimestamp();
+ });
+
+ return $calendarObjects;
+ }
+
+ private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
+ $calendarObjects = [];
+ $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
+
+ $result = $query->executeQuery();
+
+ while (($row = $result->fetch()) !== false) {
+ if ($filterByTimeRange === false) {
+ // No filter required
+ $calendarObjects[] = $row;
+ continue;
+ }
+
+ try {
+ $isValid = $this->validateFilterForObject($row, [
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $start,
+ 'end' => $end,
+ ],
+ ],
+ ],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+ } catch (MaxInstancesExceededException $ex) {
+ $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
+ 'app' => 'dav',
+ 'exception' => $ex,
+ ]);
+ continue;
+ }
+
+ if (is_resource($row['calendardata'])) {
+ // Put the stream back to the beginning so it can be read another time
+ rewind($row['calendardata']);
+ }
+
+ if ($isValid) {
+ $calendarObjects[] = $row;
+ }
+ }
+
+ $result->closeCursor();
+
+ return $calendarObjects;
}
/**
@@ -2100,22 +2277,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
array $componentTypes,
array $searchProperties,
array $searchParameters,
- array $options = []
+ array $options = [],
): array {
return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
$calendarObjectIdQuery = $this->db->getQueryBuilder();
- $calendarOr = $calendarObjectIdQuery->expr()->orX();
- $searchOr = $calendarObjectIdQuery->expr()->orX();
+ $calendarOr = [];
+ $searchOr = [];
// Fetch calendars and subscription
$calendars = $this->getCalendarsForUser($principalUri);
$subscriptions = $this->getSubscriptionsForUser($principalUri);
foreach ($calendars as $calendar) {
- $calendarAnd = $calendarObjectIdQuery->expr()->andX();
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ $calendarAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
+ $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
+ );
// If it's shared, limit search to public events
if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
@@ -2123,12 +2301,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
- $calendarOr->add($calendarAnd);
+ $calendarOr[] = $calendarAnd;
}
foreach ($subscriptions as $subscription) {
- $subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
+ $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
+ $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
+ );
// If it's shared, limit search to public events
if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
@@ -2136,28 +2315,30 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
- $calendarOr->add($subscriptionAnd);
+ $calendarOr[] = $subscriptionAnd;
}
foreach ($searchProperties as $property) {
- $propertyAnd = $calendarObjectIdQuery->expr()->andX();
- $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
- $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
+ $propertyAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
+ );
- $searchOr->add($propertyAnd);
+ $searchOr[] = $propertyAnd;
}
foreach ($searchParameters as $property => $parameter) {
- $parameterAnd = $calendarObjectIdQuery->expr()->andX();
- $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
- $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
+ $parameterAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)),
+ );
- $searchOr->add($parameterAnd);
+ $searchOr[] = $parameterAnd;
}
- if ($calendarOr->count() === 0) {
+ if (empty($calendarOr)) {
return [];
}
- if ($searchOr->count() === 0) {
+ if (empty($searchOr)) {
return [];
}
@@ -2165,8 +2346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->from($this->dbObjectPropertiesTable, 'cob')
->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
- ->andWhere($calendarOr)
- ->andWhere($searchOr)
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr))
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
if ($pattern !== '') {
@@ -2201,7 +2382,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$result = $calendarObjectIdQuery->executeQuery();
$matches = [];
while (($row = $result->fetch()) !== false) {
- $matches[] = (int) $row['objectid'];
+ $matches[] = (int)$row['objectid'];
}
$result->closeCursor();
@@ -2288,7 +2469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
+ 'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
];
}
@@ -2362,74 +2543,57 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
);
$stmt = $qb->executeQuery();
$currentToken = $stmt->fetchOne();
+ $initialSync = !is_numeric($syncToken);
if ($currentToken === false) {
return null;
}
- $result = [
- 'syncToken' => $currentToken,
- 'added' => [],
- 'modified' => [],
- 'deleted' => [],
- ];
-
- if ($syncToken) {
- $qb = $this->db->getQueryBuilder();
-
- $qb->select('uri', 'operation')
- ->from('calendarchanges')
- ->where(
- $qb->expr()->andX(
- $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
- $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
- )
- )->orderBy('synctoken');
- if (is_int($limit) && $limit > 0) {
- $qb->setMaxResults($limit);
- }
-
- // Fetching all changes
- $stmt = $qb->executeQuery();
- $changes = [];
-
- // This loop ensures that any duplicates are overwritten, only the
- // last change on a node is relevant.
- while ($row = $stmt->fetch()) {
- $changes[$row['uri']] = $row['operation'];
- }
- $stmt->closeCursor();
-
- foreach ($changes as $uri => $operation) {
- switch ($operation) {
- case 1:
- $result['added'][] = $uri;
- break;
- case 2:
- $result['modified'][] = $uri;
- break;
- case 3:
- $result['deleted'][] = $uri;
- break;
- }
- }
- } else {
- // No synctoken supplied, this is the initial sync.
+ // evaluate if this is a initial sync and construct appropriate command
+ if ($initialSync) {
$qb = $this->db->getQueryBuilder();
$qb->select('uri')
->from('calendarobjects')
- ->where(
- $qb->expr()->andX(
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
- )
- );
- $stmt = $qb->executeQuery();
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ } else {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('uri', $qb->func()->max('operation'))
+ ->from('calendarchanges')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)))
+ ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)))
+ ->groupBy('uri');
+ }
+ // evaluate if limit exists
+ if (is_numeric($limit)) {
+ $qb->setMaxResults($limit);
+ }
+ // execute command
+ $stmt = $qb->executeQuery();
+ // build results
+ $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve results
+ if ($initialSync) {
$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
- $stmt->closeCursor();
+ } else {
+ // \PDO::FETCH_NUM is needed due to the inconsistent field names
+ // produced by doctrine for MAX() with different databases
+ while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
+ // assign uri (column 0) to appropriate mutation based on operation (column 1)
+ // forced (int) is needed as doctrine with OCI returns the operation field as string not integer
+ match ((int)$entry[1]) {
+ 1 => $result['added'][] = $entry[0],
+ 2 => $result['modified'][] = $entry[0],
+ 3 => $result['deleted'][] = $entry[0],
+ default => $this->logger->debug('Unknown calendar change operation detected')
+ };
+ }
}
+ $stmt->closeCursor();
+
return $result;
}, $this->db);
}
@@ -2492,7 +2656,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'lastmodified' => $row['lastmodified'],
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
];
$subscriptions[] = $this->rowToSubscription($row, $subscription);
@@ -2614,7 +2778,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSubscription($subscriptionId) {
- $this->atomic(function () use ($subscriptionId) {
+ $this->atomic(function () use ($subscriptionId): void {
$subscriptionRow = $this->getSubscriptionById($subscriptionId);
$query = $this->db->getQueryBuilder();
@@ -2697,9 +2861,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
public function getSchedulingObjects($principalUri) {
$query = $this->db->getQueryBuilder();
$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
- ->from('schedulingobjects')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
- ->executeQuery();
+ ->from('schedulingobjects')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->executeQuery();
$results = [];
while (($row = $stmt->fetch()) !== false) {
@@ -2727,9 +2891,47 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->delete('schedulingobjects')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
- ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
- ->executeStatement();
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->executeStatement();
+ }
+
+ /**
+ * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
+ *
+ * @param int $modifiedBefore
+ * @param int $limit
+ * @return void
+ */
+ public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('id')
+ ->from('schedulingobjects')
+ ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
+ ->setMaxResults($limit);
+ $result = $query->executeQuery();
+ $count = $result->rowCount();
+ if ($count === 0) {
+ return;
+ }
+ $ids = array_map(static function (array $id) {
+ return (int)$id[0];
+ }, $result->fetchAll(\PDO::FETCH_NUM));
+ $result->closeCursor();
+
+ $numDeleted = 0;
+ $deleteQuery = $this->db->getQueryBuilder();
+ $deleteQuery->delete('schedulingobjects')
+ ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
+ foreach (array_chunk($ids, 1000) as $chunk) {
+ $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $numDeleted += $deleteQuery->executeStatement();
+ }
+
+ if ($numDeleted === $limit) {
+ $this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
+ $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
+ }
}
/**
@@ -2768,7 +2970,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->cachedObjects = [];
$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table) {
+ $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
$query = $this->db->getQueryBuilder();
$query->select('synctoken')
->from($table)
@@ -2785,7 +2987,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendarid' => $query->createNamedParameter($calendarId),
'operation' => $query->createNamedParameter($operation),
'calendartype' => $query->createNamedParameter($calendarType),
- 'created_at' => time(),
+ 'created_at' => $query->createNamedParameter(time()),
]);
foreach ($objectUris as $uri) {
$query->setParameter('uri', $uri);
@@ -2803,7 +3005,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $calendarType) {
+ $this->atomic(function () use ($calendarId, $calendarType): void {
$qbAdded = $this->db->getQueryBuilder();
$qbAdded->select('uri')
->from('calendarobjects')
@@ -2834,7 +3036,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
);
$resultDeleted = $qbDeleted->executeQuery();
$deletedUris = array_map(function (string $uri) {
- return str_replace("-deleted.ics", ".ics", $uri);
+ return str_replace('-deleted.ics', '.ics', $uri);
}, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
$resultDeleted->closeCursor();
$this->addChanges($calendarId, $deletedUris, 3, $calendarType);
@@ -2906,7 +3108,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$lastOccurrence = $firstOccurrence;
}
} else {
- $it = new EventIterator($vEvents);
+ try {
+ $it = new EventIterator($vEvents);
+ } catch (NoInstancesException $e) {
+ $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
+ 'app' => 'dav',
+ 'exception' => $e,
+ ]);
+ throw new Forbidden($e->getMessage());
+ }
$maxDate = new DateTime(self::MAX_DATE);
$firstOccurrence = $it->getDtStart()->getTimestamp();
if ($it->isInfinite()) {
@@ -2961,7 +3171,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param list<string> $remove
*/
public function updateShares(IShareable $shareable, array $add, array $remove): void {
- $this->atomic(function () use ($shareable, $add, $remove) {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
$calendarId = $shareable->getResourceId();
$calendarRow = $this->getCalendarById($calendarId);
if ($calendarRow === null) {
@@ -2988,7 +3198,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @param boolean $value
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return string|null
*/
public function setPublishStatus($value, $calendar) {
@@ -3023,7 +3233,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return mixed
*/
public function getPublishStatus($calendar) {
@@ -3059,7 +3269,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) {
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
try {
@@ -3100,7 +3310,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$query->setParameter('name', $property->name);
$query->setParameter('parameter', null);
- $query->setParameter('value', $value);
+ $query->setParameter('value', mb_strcut($value, 0, 254));
$query->executeStatement();
}
@@ -3131,7 +3341,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* deletes all birthday calendars
*/
public function deleteAllBirthdayCalendars() {
- $this->atomic(function () {
+ $this->atomic(function (): void {
$query = $this->db->getQueryBuilder();
$result = $query->select(['id'])->from('calendars')
->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
@@ -3151,7 +3361,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param $subscriptionId
*/
public function purgeAllCachedEventsForSubscription($subscriptionId) {
- $this->atomic(function () use ($subscriptionId) {
+ $this->atomic(function () use ($subscriptionId): void {
$query = $this->db->getQueryBuilder();
$query->select('uri')
->from('calendarobjects')
@@ -3188,6 +3398,45 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * @param int $subscriptionId
+ * @param array<int> $calendarObjectIds
+ * @param array<string> $calendarObjectUris
+ */
+ public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
+ if (empty($calendarObjectUris)) {
+ return;
+ }
+
+ $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
+ foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->dbObjectPropertiesTable)
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
+ ->executeStatement();
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
+ ->executeStatement();
+ }
+
+ foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarchanges')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->executeStatement();
+ }
+ $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
+ }, $this->db);
+ }
+
+ /**
* Move a calendar from one user to another
*
* @param string $uriName
@@ -3270,7 +3519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->from('calendarchanges');
$result = $query->executeQuery();
- $maxId = (int) $result->fetchOne();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
if (!$maxId || $maxId < $keep) {
return 0;
@@ -3374,4 +3623,68 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
return $subscription;
}
+
+ /**
+ * delete all invitations from a given calendar
+ *
+ * @since 31.0.0
+ *
+ * @param int $calendarId
+ *
+ * @return void
+ */
+ protected function purgeCalendarInvitations(int $calendarId): void {
+ // select all calendar object uid's
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->select('uid')
+ ->from($this->dbObjectsTable)
+ ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId)));
+ $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ // delete all links that match object uid's
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->delete($this->dbObjectInvitationsTable)
+ ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY));
+ foreach (array_chunk($allIds, 1000) as $chunkIds) {
+ $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
+ $cmd->executeStatement();
+ }
+ }
+
+ /**
+ * Delete all invitations from a given calendar event
+ *
+ * @since 31.0.0
+ *
+ * @param string $eventId UID of the event
+ *
+ * @return void
+ */
+ protected function purgeObjectInvitations(string $eventId): void {
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->delete($this->dbObjectInvitationsTable)
+ ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
+ $cmd->executeStatement();
+ }
+
+ public function unshare(IShareable $shareable, string $principal): void {
+ $this->atomic(function () use ($shareable, $principal): void {
+ $calendarData = $this->getCalendarById($shareable->getResourceId());
+ if ($calendarData === null) {
+ throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $shareable->getResourceId());
+ }
+
+ $oldShares = $this->getShares($shareable->getResourceId());
+ $unshare = $this->calendarSharingBackend->unshare($shareable, $principal);
+
+ if ($unshare) {
+ $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent(
+ $shareable->getResourceId(),
+ $calendarData,
+ $oldShares,
+ [],
+ [$principal]
+ ));
+ }
+ }, $this->db);
+ }
}
diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php
index fbfbdf652ec..dd3a4cf3f69 100644
--- a/apps/dav/lib/CalDAV/Calendar.php
+++ b/apps/dav/lib/CalDAV/Calendar.php
@@ -1,30 +1,10 @@
<?php
+
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -51,12 +31,16 @@ use Sabre\DAV\PropPatch;
* @property CalDavBackend $caldavBackend
*/
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
- private IConfig $config;
protected IL10N $l10n;
private bool $useTrashbin = true;
- private LoggerInterface $logger;
- public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
+ public function __construct(
+ BackendInterface $caldavBackend,
+ $calendarInfo,
+ IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
// Convert deletion date to ISO8601 string
if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
$calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
@@ -66,17 +50,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
parent::__construct($caldavBackend, $calendarInfo);
- if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
+ if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
}
- if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
- $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
+ if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI
+ && $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
}
-
- $this->config = $config;
$this->l10n = $l10n;
- $this->logger = $logger;
}
/**
@@ -209,8 +190,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
$acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
$allowedPrincipals = [
$this->getOwner(),
- $this->getOwner(). '/calendar-proxy-read',
- $this->getOwner(). '/calendar-proxy-write',
+ $this->getOwner() . '/calendar-proxy-read',
+ $this->getOwner() . '/calendar-proxy-write',
parent::getOwner(),
'principals/system/public'
];
@@ -233,12 +214,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
}
public function delete() {
- if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
- $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
- $principal = 'principal:' . parent::getOwner();
- $this->caldavBackend->updateShares($this, [], [
- $principal
- ]);
+ if ($this->isShared()) {
+ $this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI());
return;
}
@@ -396,7 +373,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
* @inheritDoc
*/
public function restore(): void {
- $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
+ $this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
}
public function disableTrashbin(): void {
@@ -410,9 +387,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
if (!($sourceNode instanceof CalendarObject)) {
return false;
}
-
try {
- return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
+ return $this->caldavBackend->moveCalendarObject(
+ $sourceNode->getOwner(),
+ $sourceNode->getId(),
+ $this->getOwner(),
+ $this->getResourceId(),
+ $targetName,
+ );
} catch (Exception $e) {
$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
return false;
diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php
index cbf5cebc9e7..89b78ba9007 100644
--- a/apps/dav/lib/CalDAV/CalendarHome.php
+++ b/apps/dav/lib/CalDAV/CalendarHome.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -30,6 +11,10 @@ use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
+use OCP\App\IAppManager;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\CalDAV\Backend\NotificationSupport;
@@ -44,31 +29,29 @@ use Sabre\DAV\MkCol;
class CalendarHome extends \Sabre\CalDAV\CalendarHome {
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
- /** @var \OCP\IConfig */
+ /** @var IConfig */
private $config;
/** @var PluginManager */
private $pluginManager;
-
- /** @var bool */
- private $returnCachedSubscriptions = false;
-
- /** @var LoggerInterface */
- private $logger;
private ?array $cachedChildren = null;
- public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) {
+ public function __construct(
+ BackendInterface $caldavBackend,
+ array $principalInfo,
+ private LoggerInterface $logger,
+ private bool $returnCachedSubscriptions,
+ ) {
parent::__construct($caldavBackend, $principalInfo);
$this->l10n = \OC::$server->getL10N('dav');
- $this->config = \OC::$server->getConfig();
+ $this->config = Server::get(IConfig::class);
$this->pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ Server::get(IAppManager::class)
);
- $this->logger = $logger;
}
/**
@@ -166,9 +149,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
// Calendar - this covers all "regular" calendars, but not shared
// only check if the method is available
- if($this->caldavBackend instanceof CalDavBackend) {
+ if ($this->caldavBackend instanceof CalDavBackend) {
$calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name);
- if(!empty($calendar)) {
+ if (!empty($calendar)) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
}
@@ -219,8 +202,4 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
$principalUri = $this->principalInfo['uri'];
return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset);
}
-
- public function enableCachedSubscriptionsForThisRequest() {
- $this->returnCachedSubscriptions = true;
- }
}
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php
index 397cff38037..5f912da732e 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -3,34 +3,20 @@
declare(strict_types=1);
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
+use Generator;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Exceptions\CalendarException;
+use OCP\Calendar\ICalendarExport;
+use OCP\Calendar\ICalendarIsEnabled;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Constants;
@@ -44,18 +30,13 @@ use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
-class CalendarImpl implements ICreateFromString, IHandleImipMessage {
- private CalDavBackend $backend;
- private Calendar $calendar;
- /** @var array<string, mixed> */
- private array $calendarInfo;
-
- public function __construct(Calendar $calendar,
- array $calendarInfo,
- CalDavBackend $backend) {
- $this->calendar = $calendar;
- $this->calendarInfo = $calendarInfo;
- $this->backend = $backend;
+class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled {
+ public function __construct(
+ private Calendar $calendar,
+ /** @var array<string, mixed> */
+ private array $calendarInfo,
+ private CalDavBackend $backend,
+ ) {
}
/**
@@ -63,7 +44,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
* @since 13.0.0
*/
public function getKey(): string {
- return (string) $this->calendarInfo['id'];
+ return (string)$this->calendarInfo['id'];
}
/**
@@ -104,7 +85,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
/** @var VCalendar $vobj */
$vobj = Reader::read($timezoneProp);
$components = $vobj->getComponents();
- if(empty($components)) {
+ if (empty($components)) {
return null;
}
/** @var VTimeZone $vtimezone */
@@ -112,16 +93,6 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
return $vtimezone;
}
- /**
- * @param string $pattern which should match within the $searchProperties
- * @param array $searchProperties defines the properties within the query pattern should match
- * @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
- * @param int|null $limit - limit number of search results
- * @param int|null $offset - offset for paging of search results
- * @return array an array of events/journals/todos which are arrays of key-value-pairs
- * @since 13.0.0
- */
public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
return $this->backend->search($this->calendarInfo, $pattern,
$searchProperties, $options, $limit, $offset);
@@ -135,6 +106,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
$permissions = $this->calendar->getACL();
$result = 0;
foreach ($permissions as $permission) {
+ if ($this->calendarInfo['principaluri'] !== $permission['principal']) {
+ continue;
+ }
+
switch ($permission['privilege']) {
case '{DAV:}read':
$result |= Constants::PERMISSION_READ;
@@ -153,6 +128,20 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
/**
+ * @since 32.0.0
+ */
+ public function isEnabled(): bool {
+ return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
+ }
+
+ /**
+ * @since 31.0.0
+ */
+ public function isWritable(): bool {
+ return $this->calendar->canWrite();
+ }
+
+ /**
* @since 26.0.0
*/
public function isDeleted(): bool {
@@ -160,19 +149,22 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
/**
- * Create a new calendar event for this calendar
- * by way of an ICS string
- *
- * @param string $name the file name - needs to contain the .ics ending
- * @param string $calendarData a string containing a valid VEVENT ics
- *
- * @throws CalendarException
+ * @since 31.0.0
*/
- public function createFromString(string $name, string $calendarData): void {
- $server = new InvitationResponseServer(false);
+ public function isShared(): bool {
+ return $this->calendar->isShared();
+ }
+ /**
+ * @throws CalendarException
+ */
+ private function createFromStringInServer(
+ string $name,
+ string $calendarData,
+ \OCA\DAV\Connector\Sabre\Server $server,
+ ): void {
/** @var CustomPrincipalPlugin $plugin */
- $plugin = $server->getServer()->getPlugin('auth');
+ $plugin = $server->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
@@ -188,14 +180,14 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
- $schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
+ $schedulingPlugin = $server->getPlugin('caldav-schedule');
$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
$stream = fopen('php://memory', 'rb+');
fwrite($stream, $calendarData);
rewind($stream);
try {
- $server->getServer()->createFile($fullCalendarFilename, $stream);
+ $server->createFile($fullCalendarFilename, $stream);
} catch (Conflict $e) {
throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
} finally {
@@ -203,6 +195,16 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
}
+ public function createFromString(string $name, string $calendarData): void {
+ $server = new EmbeddedCalDavServer(false);
+ $this->createFromStringInServer($name, $calendarData, $server->getServer());
+ }
+
+ public function createFromStringMinimal(string $name, string $calendarData): void {
+ $server = new InvitationResponseServer(false);
+ $this->createFromStringInServer($name, $calendarData, $server->getServer());
+ }
+
/**
* @throws CalendarException
*/
@@ -240,7 +242,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
$attendee = $vEvent->{'ATTENDEE'}->getValue();
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
- if ($iTipMessage->method === 'REPLY') {
+ if ($iTipMessage->method === 'REQUEST') {
+ $iTipMessage->sender = $organizer;
+ $iTipMessage->recipient = $attendee;
+ } elseif ($iTipMessage->method === 'REPLY') {
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
$iTipMessage->recipient = $organizer;
} else {
@@ -261,4 +266,27 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}
+
+ /**
+ * Export objects
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
+ */
+ public function export(?CalendarExportOptions $options = null): Generator {
+ foreach (
+ $this->backend->exportCalendar(
+ $this->calendarInfo['id'],
+ $this->backend::CALENDAR_TYPE_CALENDAR,
+ $options
+ ) as $event
+ ) {
+ $vObject = Reader::read($event['calendardata']);
+ if ($vObject instanceof VCalendar) {
+ yield $vObject;
+ }
+ }
+ }
+
}
diff --git a/apps/dav/lib/CalDAV/CalendarManager.php b/apps/dav/lib/CalDAV/CalendarManager.php
index daa96a51392..a2d2f1cda8a 100644
--- a/apps/dav/lib/CalDAV/CalendarManager.php
+++ b/apps/dav/lib/CalDAV/CalendarManager.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -31,18 +13,6 @@ use Psr\Log\LoggerInterface;
class CalendarManager {
- /** @var CalDavBackend */
- private $backend;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
/**
* CalendarManager constructor.
*
@@ -50,11 +20,12 @@ class CalendarManager {
* @param IL10N $l10n
* @param IConfig $config
*/
- public function __construct(CalDavBackend $backend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
- $this->backend = $backend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $backend,
+ private IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php
index 0d82e5ffa76..02178b4236f 100644
--- a/apps/dav/lib/CalDAV/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/CalendarObject.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- * @copyright Copyright (c) 2020, Gary Kim <gary@garykim.dev>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -33,9 +14,6 @@ use Sabre\VObject\Reader;
class CalendarObject extends \Sabre\CalDAV\CalendarObject {
- /** @var IL10N */
- protected $l10n;
-
/**
* CalendarObject constructor.
*
@@ -44,16 +22,17 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param array $calendarInfo
* @param array $objectData
*/
- public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
+ public function __construct(
+ CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
array $calendarInfo,
- array $objectData) {
+ array $objectData,
+ ) {
parent::__construct($caldavBackend, $calendarInfo, $objectData);
if ($this->isShared()) {
unset($this->objectData['size']);
}
-
- $this->l10n = $l10n;
}
/**
@@ -82,7 +61,7 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
}
public function getId(): int {
- return (int) $this->objectData['id'];
+ return (int)$this->objectData['id'];
}
protected function isShared() {
@@ -97,19 +76,16 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param Component\VCalendar $vObject
* @return void
*/
- private function createConfidentialObject(Component\VCalendar $vObject) {
+ private function createConfidentialObject(Component\VCalendar $vObject): void {
/** @var Component $vElement */
- $vElement = null;
- if (isset($vObject->VEVENT)) {
- $vElement = $vObject->VEVENT;
- }
- if (isset($vObject->VJOURNAL)) {
- $vElement = $vObject->VJOURNAL;
- }
- if (isset($vObject->VTODO)) {
- $vElement = $vObject->VTODO;
- }
- if (!is_null($vElement)) {
+ $vElements = array_filter($vObject->getComponents(), static function ($vElement) {
+ return $vElement instanceof Component\VEvent || $vElement instanceof Component\VJournal || $vElement instanceof Component\VTodo;
+ });
+
+ foreach ($vElements as $vElement) {
+ if (empty($vElement->select('SUMMARY'))) {
+ $vElement->add('SUMMARY', $this->l10n->t('Busy')); // This is needed to mask "Untitled Event" events
+ }
foreach ($vElement->children() as &$property) {
/** @var Property $property */
switch ($property->name) {
diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php
index f29c601db2d..3cc4039ed36 100644
--- a/apps/dav/lib/CalDAV/CalendarProvider.php
+++ b/apps/dav/lib/CalDAV/CalendarProvider.php
@@ -3,28 +3,13 @@
declare(strict_types=1);
/**
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
+use OCA\DAV\Db\Property;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Calendar\ICalendarProvider;
use OCP\IConfig;
use OCP\IL10N;
@@ -32,39 +17,28 @@ use Psr\Log\LoggerInterface;
class CalendarProvider implements ICalendarProvider {
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(CalDavBackend $calDavBackend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
- $this->calDavBackend = $calDavBackend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private PropertyMapper $propertyMapper,
+ ) {
}
public function getCalendars(string $principalUri, array $calendarUris = []): array {
- $calendarInfos = [];
- if (empty($calendarUris)) {
- $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri);
- } else {
- foreach ($calendarUris as $calendarUri) {
- $calendarInfos[] = $this->calDavBackend->getCalendarByUri($principalUri, $calendarUri);
- }
- }
- $calendarInfos = array_filter($calendarInfos);
+ $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri) ?? [];
+
+ if (!empty($calendarUris)) {
+ $calendarInfos = array_filter($calendarInfos, function ($calendar) use ($calendarUris) {
+ return in_array($calendar['uri'], $calendarUris);
+ });
+ }
$iCalendars = [];
foreach ($calendarInfos as $calendarInfo) {
+ $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri']));
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$iCalendars[] = new CalendarImpl(
$calendar,
@@ -74,4 +48,23 @@ class CalendarProvider implements ICalendarProvider {
}
return $iCalendars;
}
+
+ public function getAdditionalProperties(string $principalUri, string $calendarUri): array {
+ $user = str_replace('principals/users/', '', $principalUri);
+ $path = 'calendars/' . $user . '/' . $calendarUri;
+
+ $properties = $this->propertyMapper->findPropertiesByPath($user, $path);
+
+ $list = [];
+ foreach ($properties as $property) {
+ if ($property instanceof Property) {
+ $list[$property->getPropertyname()] = match ($property->getPropertyname()) {
+ '{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(),
+ default => $property->getPropertyvalue()
+ };
+ }
+ }
+
+ return $list;
+ }
}
diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php
index 0c701d9cdcf..c0a313955bb 100644
--- a/apps/dav/lib/CalDAV/CalendarRoot.php
+++ b/apps/dav/lib/CalDAV/CalendarRoot.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -30,25 +12,29 @@ use Sabre\CalDAV\Backend;
use Sabre\DAVACL\PrincipalBackend;
class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
- private LoggerInterface $logger;
+ private array $returnCachedSubscriptions = [];
public function __construct(
PrincipalBackend\BackendInterface $principalBackend,
Backend\BackendInterface $caldavBackend,
$principalPrefix,
- LoggerInterface $logger
+ private LoggerInterface $logger,
) {
parent::__construct($principalBackend, $caldavBackend, $principalPrefix);
- $this->logger = $logger;
}
public function getChildForPrincipal(array $principal) {
- return new CalendarHome($this->caldavBackend, $principal, $this->logger);
+ return new CalendarHome(
+ $this->caldavBackend,
+ $principal,
+ $this->logger,
+ array_key_exists($principal['uri'], $this->returnCachedSubscriptions)
+ );
}
public function getName() {
- if ($this->principalPrefix === 'principals/calendar-resources' ||
- $this->principalPrefix === 'principals/calendar-rooms') {
+ if ($this->principalPrefix === 'principals/calendar-resources'
+ || $this->principalPrefix === 'principals/calendar-rooms') {
$parts = explode('/', $this->principalPrefix);
return $parts[1];
@@ -56,4 +42,8 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
return parent::getName();
}
+
+ public function enableReturnCachedSubscriptions(string $principalUri): void {
+ $this->returnCachedSubscriptions['principals/users/' . $principalUri] = true;
+ }
}
diff --git a/apps/dav/lib/CalDAV/DefaultCalendarValidator.php b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php
new file mode 100644
index 00000000000..266e07ef255
--- /dev/null
+++ b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\DAV\Exception as DavException;
+
+class DefaultCalendarValidator {
+ /**
+ * Check if a given Calendar node is suitable to be used as the default calendar for scheduling.
+ *
+ * @throws DavException If the calendar is not suitable to be used as the default calendar
+ */
+ public function validateScheduleDefaultCalendar(Calendar $calendar): void {
+ // Sanity checks for a calendar that should handle invitations
+ if ($calendar->isSubscription()
+ || !$calendar->canWrite()
+ || $calendar->isShared()
+ || $calendar->isDeleted()) {
+ throw new DavException('Calendar is a subscription, not writable, shared or deleted');
+ }
+
+ // Calendar must support VEVENTs
+ $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ $calendarProperties = $calendar->getProperties([$sCCS]);
+ if (isset($calendarProperties[$sCCS])) {
+ $supportedComponents = $calendarProperties[$sCCS]->getValue();
+ } else {
+ $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
+ }
+ if (!in_array('VEVENT', $supportedComponents, true)) {
+ throw new DavException('Calendar does not support VEVENT components');
+ }
+ }
+}
diff --git a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php
new file mode 100644
index 00000000000..21d8c06fa99
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
+use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
+use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
+use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
+use OCA\DAV\Connector\Sabre\CachingTree;
+use OCA\DAV\Connector\Sabre\DavAclPlugin;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Connector\Sabre\LockPlugin;
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
+use OCA\DAV\Events\SabrePluginAuthInitEvent;
+use OCA\DAV\RootCollection;
+use OCA\Theming\ThemingDefaults;
+use OCP\App\IAppManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use OCP\L10N\IFactory as IL10NFactory;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+class EmbeddedCalDavServer {
+ private readonly \OCA\DAV\Connector\Sabre\Server $server;
+
+ public function __construct(bool $public = true) {
+ $baseUri = \OC::$WEBROOT . '/remote.php/dav/';
+ $logger = Server::get(LoggerInterface::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
+ $appConfig = Server::get(IAppConfig::class);
+ $l10nFactory = Server::get(IL10NFactory::class);
+ $l10n = $l10nFactory->get('dav');
+
+ $root = new RootCollection();
+ $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
+
+ // Add maintenance plugin
+ $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), $l10n));
+
+ // Set URL explicitly due to reverse-proxy situations
+ $this->server->httpRequest->setUrl($baseUri);
+ $this->server->setBaseUri($baseUri);
+
+ $this->server->addPlugin(new BlockLegacyClientPlugin(
+ Server::get(IConfig::class),
+ Server::get(ThemingDefaults::class),
+ ));
+ $this->server->addPlugin(new AnonymousOptionsPlugin());
+
+ // allow custom principal uri option
+ if ($public) {
+ $this->server->addPlugin(new PublicPrincipalPlugin());
+ } else {
+ $this->server->addPlugin(new CustomPrincipalPlugin());
+ }
+
+ // allow setup of additional auth backends
+ $event = new SabrePluginAuthInitEvent($this->server);
+ $dispatcher->dispatchTyped($event);
+
+ $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
+ $this->server->addPlugin(new LockPlugin());
+ $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
+
+ // acl
+ $acl = new DavAclPlugin();
+ $acl->principalCollectionSet = [
+ 'principals/users', 'principals/groups'
+ ];
+ $this->server->addPlugin($acl);
+
+ // calendar plugins
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class)));
+ $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
+ //$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
+ $this->server->addPlugin(new PublishPlugin(
+ Server::get(IConfig::class),
+ Server::get(IURLGenerator::class)
+ ));
+ if ($appConfig->getValueString('dav', 'sendInvitations', 'yes') === 'yes') {
+ $this->server->addPlugin(Server::get(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class));
+ }
+
+ // wait with registering these until auth is handled and the filesystem is setup
+ $this->server->on('beforeMethod:*', function () use ($root): void {
+ // register plugins from apps
+ $pluginManager = new PluginManager(
+ \OC::$server,
+ Server::get(IAppManager::class)
+ );
+ foreach ($pluginManager->getAppPlugins() as $appPlugin) {
+ $this->server->addPlugin($appPlugin);
+ }
+ foreach ($pluginManager->getAppCollections() as $appCollection) {
+ $root->addChild($appCollection);
+ }
+ });
+ }
+
+ public function getServer(): \OCA\DAV\Connector\Sabre\Server {
+ return $this->server;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/EventComparisonService.php b/apps/dav/lib/CalDAV/EventComparisonService.php
index 24f5da34f8e..63395e7ce1c 100644
--- a/apps/dav/lib/CalDAV/EventComparisonService.php
+++ b/apps/dav/lib/CalDAV/EventComparisonService.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Anna Larch <anna.larch@gmx.net>
- *
- * @author 2022 Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -54,14 +37,14 @@ class EventComparisonService {
*/
private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool {
$filterEventData = [];
- foreach(self::EVENT_DIFF as $eventDiff) {
+ foreach (self::EVENT_DIFF as $eventDiff) {
$filterEventData[] = IMipService::readPropertyWithDefault($filterEvent, $eventDiff, '');
}
/** @var VEvent $component */
foreach ($eventsToFilter as $k => $eventToFilter) {
$eventToFilterData = [];
- foreach(self::EVENT_DIFF as $eventDiff) {
+ foreach (self::EVENT_DIFF as $eventDiff) {
$eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, '');
}
// events are identical and can be removed
@@ -90,23 +73,23 @@ class EventComparisonService {
$newEventComponents = $new->getComponents();
foreach ($newEventComponents as $k => $event) {
- if(!$event instanceof VEvent) {
+ if (!$event instanceof VEvent) {
unset($newEventComponents[$k]);
}
}
- if(empty($old)) {
+ if (empty($old)) {
return ['old' => null, 'new' => $newEventComponents];
}
$oldEventComponents = $old->getComponents();
- if(is_array($oldEventComponents) && !empty($oldEventComponents)) {
+ if (is_array($oldEventComponents) && !empty($oldEventComponents)) {
foreach ($oldEventComponents as $k => $event) {
- if(!$event instanceof VEvent) {
+ if (!$event instanceof VEvent) {
unset($oldEventComponents[$k]);
continue;
}
- if($this->removeIfUnchanged($event, $newEventComponents)) {
+ if ($this->removeIfUnchanged($event, $newEventComponents)) {
unset($oldEventComponents[$k]);
}
}
diff --git a/apps/dav/lib/CalDAV/EventReader.php b/apps/dav/lib/CalDAV/EventReader.php
new file mode 100644
index 00000000000..ee2b8f33f9a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReader.php
@@ -0,0 +1,771 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeImmutable;
+use DateTimeInterface;
+use DateTimeZone;
+use InvalidArgumentException;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Reader;
+
+class EventReader {
+
+ protected VEvent $baseEvent;
+ protected DateTimeInterface $baseEventStartDate;
+ protected DateTimeZone $baseEventStartTimeZone;
+ protected DateTimeInterface $baseEventEndDate;
+ protected DateTimeZone $baseEventEndTimeZone;
+ protected bool $baseEventStartDateFloating = false;
+ protected bool $baseEventEndDateFloating = false;
+ protected int $baseEventDuration;
+
+ protected ?EventReaderRRule $rruleIterator = null;
+ protected ?EventReaderRDate $rdateIterator = null;
+ protected ?EventReaderRRule $eruleIterator = null;
+ protected ?EventReaderRDate $edateIterator = null;
+
+ protected array $recurrenceModified;
+ protected ?DateTimeInterface $recurrenceCurrentDate;
+
+ protected array $dayNamesMap = [
+ 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
+ ];
+ protected array $monthNamesMap = [
+ 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
+ 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
+ ];
+ protected array $relativePositionNamesMap = [
+ 1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifth',
+ -1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifth Last'
+ ];
+
+ /**
+ * Initilizes the Event Reader
+ *
+ * There is several ways to set up the iterator.
+ *
+ * 1. You can pass a VCALENDAR component (as object or string) and a UID.
+ * 2. You can pass an array of VEVENTs (all UIDS should match).
+ * 3. You can pass a single VEVENT component (as object or string).
+ *
+ * Only the second method is recommended. The other 1 and 3 will be removed
+ * at some point in the future.
+ *
+ * The $uid parameter is only required for the first method.
+ *
+ * @since 30.0.0
+ *
+ * @param VCalendar|VEvent|Array|String $input
+ * @param string|null $uid
+ * @param DateTimeZone|null $timeZone reference timezone for floating dates and times
+ */
+ public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {
+
+ $timeZoneFactory = new TimeZoneFactory();
+
+ // evaluate if the input is a string and convert it to and vobject if required
+ if (is_string($input)) {
+ $input = Reader::read($input);
+ }
+ // evaluate if input is a single event vobject and convert it to a collection
+ if ($input instanceof VEvent) {
+ $events = [$input];
+ }
+ // evaluate if input is a calendar vobject
+ elseif ($input instanceof VCalendar) {
+ // Calendar + UID mode.
+ if ($uid === null) {
+ throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
+ }
+ // extract events from calendar
+ $events = $input->getByUID($uid);
+ // evaluate if any event where found
+ if (count($events) === 0) {
+ throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid);
+ }
+ // extract calendar timezone
+ if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
+ $calendarTimeZone = $timeZoneFactory->fromName($input->VTIMEZONE->TZID->getValue());
+ }
+ }
+ // evaluate if input is a collection of event vobjects
+ elseif (is_array($input)) {
+ $events = $input;
+ } else {
+ throw new InvalidArgumentException('Invalid input data type');
+ }
+ // find base event instance and remove it from events collection
+ foreach ($events as $key => $vevent) {
+ if (!isset($vevent->{'RECURRENCE-ID'})) {
+ $this->baseEvent = $vevent;
+ unset($events[$key]);
+ }
+ }
+
+ // No base event was found. CalDAV does allow cases where only
+ // overridden instances are stored.
+ //
+ // In this particular case, we're just going to grab the first
+ // event and use that instead. This may not always give the
+ // desired result.
+ if (!isset($this->baseEvent) && count($events) > 0) {
+ $this->baseEvent = array_shift($events);
+ }
+
+ // determine the event starting time zone
+ // we require this to align all other dates times
+ // evaluate if timezone parameter was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventStartTimeZone = $timeZone;
+ }
+ // evaluate if event start date has a timezone parameter
+ elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
+ $this->baseEventStartTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTSTART->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC');
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventStartTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the UTC timezone
+ else {
+ $this->baseEventStartTimeZone = new DateTimeZone('UTC');
+ }
+
+ // determine the event end time zone
+ // we require this to align all other dates and times
+ // evaluate if timezone parameter was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventEndTimeZone = $timeZone;
+ }
+ // evaluate if event end date has a timezone parameter
+ elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
+ $this->baseEventEndTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTEND->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC');
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventEndTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the start date time zone
+ else {
+ $this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
+ }
+ // extract start date and time
+ $this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
+ $this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
+ // determine event end date and duration
+ // evaluate if end date exists
+ // extract end date and calculate duration
+ if (isset($this->baseEvent->DTEND)) {
+ $this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
+ $this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
+ $this->baseEventDuration
+ = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp()
+ - $this->baseEventStartDate->getTimeStamp();
+ }
+ // evaluate if duration exists
+ // extract duration and calculate end date
+ elseif (isset($this->baseEvent->DURATION)) {
+ $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
+ ->add($this->baseEvent->DURATION->getDateInterval());
+ $this->baseEventDuration = $this->baseEventEndDate->getTimestamp() - $this->baseEventStartDate->getTimestamp();
+ }
+ // evaluate if start date is floating
+ // set duration to 24 hours and calculate the end date
+ // according to the rfc any event without a end date or duration is a complete day
+ elseif ($this->baseEventStartDateFloating == true) {
+ $this->baseEventDuration = 86400;
+ $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
+ ->setTimestamp($this->baseEventStartDate->getTimestamp() + $this->baseEventDuration);
+ }
+ // otherwise, set duration to zero this should never happen
+ else {
+ $this->baseEventDuration = 0;
+ $this->baseEventEndDate = $this->baseEventStartDate;
+ }
+ // evaluate if RRULE exist and construct iterator
+ if (isset($this->baseEvent->RRULE)) {
+ $this->rruleIterator = new EventReaderRRule(
+ $this->baseEvent->RRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if RDATE exist and construct iterator
+ if (isset($this->baseEvent->RDATE)) {
+ $dates = [];
+ foreach ($this->baseEvent->RDATE as $entry) {
+ $dates[] = $entry->getValue();
+ }
+ $this->rdateIterator = new EventReaderRDate(
+ implode(',', $dates),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXRULE exist and construct iterator
+ if (isset($this->baseEvent->EXRULE)) {
+ $this->eruleIterator = new EventReaderRRule(
+ $this->baseEvent->EXRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXDATE exist and construct iterator
+ if (isset($this->baseEvent->EXDATE)) {
+ $dates = [];
+ foreach ($this->baseEvent->EXDATE as $entry) {
+ $dates[] = $entry->getValue();
+ }
+ $this->edateIterator = new EventReaderRDate(
+ implode(',', $dates),
+ $this->baseEventStartDate
+ );
+ }
+ // construct collection of modified events with recurrence id as hash
+ foreach ($events as $vevent) {
+ $this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
+ }
+
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * retrieve date and time of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function startDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventStartDate);
+ }
+
+ /**
+ * retrieve time zone of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function startTimeZone(): DateTimeZone {
+ return $this->baseEventStartTimeZone;
+ }
+
+ /**
+ * retrieve date and time of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function endDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventEndDate);
+ }
+
+ /**
+ * retrieve time zone of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function endTimeZone(): DateTimeZone {
+ return $this->baseEventEndTimeZone;
+ }
+
+ /**
+ * is this an all day event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function entireDay(): bool {
+ return $this->baseEventStartDateFloating;
+ }
+
+ /**
+ * is this a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurs(): bool {
+ return ($this->rruleIterator !== null || $this->rdateIterator !== null);
+ }
+
+ /**
+ * event recurrence pattern
+ *
+ * @since 30.0.0
+ *
+ * @return string|null R - Relative or A - Absolute
+ */
+ public function recurringPattern(): ?string {
+ if ($this->rruleIterator === null && $this->rdateIterator === null) {
+ return null;
+ }
+ if ($this->rruleIterator?->isRelative()) {
+ return 'R';
+ }
+ return 'A';
+ }
+
+ /**
+ * event recurrence precision
+ *
+ * @since 30.0.0
+ *
+ * @return string|null daily, weekly, monthly, yearly, fixed
+ */
+ public function recurringPrecision(): ?string {
+ if ($this->rruleIterator !== null) {
+ return $this->rruleIterator->precision();
+ }
+ if ($this->rdateIterator !== null) {
+ return 'fixed';
+ }
+ return null;
+ }
+
+ /**
+ * event recurrence interval
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringInterval(): ?int {
+ return $this->rruleIterator?->interval();
+ }
+
+ /**
+ * event recurrence conclusion
+ *
+ * returns true if RRULE with UNTIL or COUNT (calculated) is used
+ * returns true RDATE is used
+ * returns false if RRULE or RDATE are absent, or RRRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurringConcludes(): bool {
+
+ // retrieve rrule conclusions
+ if ($this->rruleIterator?->concludesOn() !== null
+ || $this->rruleIterator?->concludesAfter() !== null) {
+ return true;
+ }
+ // retrieve rdate conclusions
+ if ($this->rdateIterator?->concludesAfter() !== null) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * event recurrence conclusion iterations
+ *
+ * returns the COUNT value if RRULE is used
+ * returns the collection count if RDATE is used
+ * returns combined count of RRULE COUNT and RDATE if both are used
+ * returns null if RRULE and RDATE are absent
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringConcludesAfter(): ?int {
+
+ // construct count place holder
+ $count = 0;
+ // retrieve and add RRULE iterations count
+ $count += (int)$this->rruleIterator?->concludesAfter();
+ // retrieve and add RDATE iterations count
+ $count += (int)$this->rdateIterator?->concludesAfter();
+ // return count
+ return !empty($count) ? $count : null;
+
+ }
+
+ /**
+ * event recurrence conclusion date
+ *
+ * returns the last date of UNTIL or COUNT (calculated) if RRULE is used
+ * returns the last date in the collection if RDATE is used
+ * returns the highest date if both RRULE and RDATE are used
+ * returns null if RRULE and RDATE are absent or RRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime|null
+ */
+ public function recurringConcludesOn(): ?DateTime {
+
+ if ($this->rruleIterator !== null) {
+ // retrieve rrule conclusion date
+ $rrule = $this->rruleIterator->concludes();
+ // evaluate if rrule conclusion is null
+ // if this is null that means the recurrence is infinate
+ if ($rrule === null) {
+ return null;
+ }
+ }
+ // retrieve rdate conclusion date
+ if ($this->rdateIterator !== null) {
+ $rdate = $this->rdateIterator->concludes();
+ }
+ // evaluate if both rrule and rdate have date
+ if (isset($rdate) && isset($rrule)) {
+ // return the highest date
+ return (($rdate > $rrule) ? $rdate : $rrule);
+ } elseif (isset($rrule)) {
+ return $rrule;
+ } elseif (isset($rdate)) {
+ return $rdate;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * event recurrence days of the week
+ *
+ * returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeek(): array {
+ // evaluate if RRULE exists and return day(s) of the week
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ }
+
+ /**
+ * event recurrence days of the week (named)
+ *
+ * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeekNamed(): array {
+ // evaluate if RRULE exists and extract day(s) of the week
+ $days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ // convert numberic month to month name
+ foreach ($days as $key => $value) {
+ $days[$key] = $this->dayNamesMap[$value];
+ }
+ // return names collection
+ return $days;
+ }
+
+ /**
+ * event recurrence days of the month
+ *
+ * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfMonth(): array {
+ // evaluate if RRULE exists and return day(s) of the month
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
+ }
+
+ /**
+ * event recurrence days of the year
+ *
+ * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfYear(): array {
+ // evaluate if RRULE exists and return day(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonth(): array {
+ // evaluate if RRULE exists and RRULE is relative return relative position(s)
+ return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month (named)
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonthNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence weeks of the year
+ *
+ * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfYear(): array {
+ // evaluate if RRULE exists and return weeks(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYear(): array {
+ // evaluate if RRULE exists and return month(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year (named)
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYearNamed(): array {
+ // evaluate if RRULE exists and extract month(s) of the year
+ $months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ // convert numberic month to month name
+ foreach ($months as $key => $value) {
+ $months[$key] = $this->monthNamesMap[$value];
+ }
+ // return months collection
+ return $months;
+ }
+
+ /**
+ * event recurrence relative positions
+ *
+ * returns collection of RRULE SETPOS value(s) [1, 5, -3]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePosition(): array {
+ // evaluate if RRULE exists and return relative position(s)
+ return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence relative positions (named)
+ *
+ * returns collection of RRULE SETPOS [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePositionNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence date
+ *
+ * returns date of currently selected recurrence
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function recurrenceDate(): ?DateTime {
+ if ($this->recurrenceCurrentDate !== null) {
+ return DateTime::createFromInterface($this->recurrenceCurrentDate);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * event recurrence rewind
+ *
+ * sets the current recurrence to the first recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceRewind(): void {
+ // rewind and increment rrule
+ if ($this->rruleIterator !== null) {
+ $this->rruleIterator->rewind();
+ }
+ // rewind and increment rdate
+ if ($this->rdateIterator !== null) {
+ $this->rdateIterator->rewind();
+ }
+ // rewind and increment exrule
+ if ($this->eruleIterator !== null) {
+ $this->eruleIterator->rewind();
+ }
+ // rewind and increment exdate
+ if ($this->edateIterator !== null) {
+ $this->edateIterator->rewind();
+ }
+ // set current date to event start date
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceAdvance(): void {
+ // place holders
+ $nextOccurrenceDate = null;
+ $nextExceptionDate = null;
+ $rruleDate = null;
+ $rdateDate = null;
+ $eruleDate = null;
+ $edateDate = null;
+ // evaludate if rrule is set and advance one interation past current date
+ if ($this->rruleIterator !== null) {
+ // forward rrule to the next future date
+ while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rruleIterator->next();
+ }
+ $rruleDate = $this->rruleIterator->current();
+ }
+ // evaludate if rdate is set and advance one interation past current date
+ if ($this->rdateIterator !== null) {
+ // forward rdate to the next future date
+ while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rdateIterator->next();
+ }
+ $rdateDate = $this->rdateIterator->current();
+ }
+ if ($rruleDate !== null && $rdateDate !== null) {
+ $nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
+ } elseif ($rruleDate !== null) {
+ $nextOccurrenceDate = $rruleDate;
+ } elseif ($rdateDate !== null) {
+ $nextOccurrenceDate = $rdateDate;
+ }
+
+ // evaludate if exrule is set and advance one interation past current date
+ if ($this->eruleIterator !== null) {
+ // forward exrule to the next future date
+ while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->eruleIterator->next();
+ }
+ $eruleDate = $this->eruleIterator->current();
+ }
+ // evaludate if exdate is set and advance one interation past current date
+ if ($this->edateIterator !== null) {
+ // forward exdate to the next future date
+ while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->edateIterator->next();
+ }
+ $edateDate = $this->edateIterator->current();
+ }
+ // evaludate if exrule and exdate are set and set nextExDate to the first next date
+ if ($eruleDate !== null && $edateDate !== null) {
+ $nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
+ } elseif ($eruleDate !== null) {
+ $nextExceptionDate = $eruleDate;
+ } elseif ($edateDate !== null) {
+ $nextExceptionDate = $edateDate;
+ }
+ // if the next date is part of exrule or exdate find another date
+ if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ $this->recurrenceAdvance();
+ } else {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ }
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection after the specific date
+ *
+ * @since 30.0.0
+ *
+ * @param DateTimeInterface $dt date and time to advance
+ *
+ * @return void
+ */
+ public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
+ while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
+ $this->recurrenceAdvance();
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRDate.php b/apps/dav/lib/CalDAV/EventReaderRDate.php
new file mode 100644
index 00000000000..20234d06c00
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRDate.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+
+class EventReaderRDate extends \Sabre\VObject\Recur\RDateIterator {
+
+ public function concludes(): ?DateTime {
+ return $this->concludesOn();
+ }
+
+ public function concludesAfter(): ?int {
+ return !empty($this->dates) ? count($this->dates) : null;
+ }
+
+ public function concludesOn(): ?DateTime {
+ if (count($this->dates) > 0) {
+ return new DateTime(
+ $this->dates[array_key_last($this->dates)],
+ $this->startDate->getTimezone()
+ );
+ }
+
+ return null;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRRule.php b/apps/dav/lib/CalDAV/EventReaderRRule.php
new file mode 100644
index 00000000000..d2b4968c479
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRRule.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeInterface;
+
+class EventReaderRRule extends \Sabre\VObject\Recur\RRuleIterator {
+
+ public function precision(): string {
+ return $this->frequency;
+ }
+
+ public function interval(): int {
+ return $this->interval;
+ }
+
+ public function concludes(): ?DateTime {
+ // evaluate if until value is a date
+ if ($this->until instanceof DateTimeInterface) {
+ return DateTime::createFromInterface($this->until);
+ }
+ // evaluate if count value is higher than 0
+ if ($this->count > 0) {
+ // temporarily store current recurrence date and counter
+ $currentReccuranceDate = $this->currentDate;
+ $currentCounter = $this->counter;
+ // iterate over occurrences until last one (subtract 2 from count for start and end occurrence)
+ while ($this->counter <= ($this->count - 2)) {
+ $this->next();
+ }
+ // temporarly store last reccurance date
+ $lastReccuranceDate = $this->currentDate;
+ // restore current recurrence date and counter
+ $this->currentDate = $currentReccuranceDate;
+ $this->counter = $currentCounter;
+ // return last recurrence date
+ return DateTime::createFromInterface($lastReccuranceDate);
+ }
+
+ return null;
+ }
+
+ public function concludesAfter(): ?int {
+ return !empty($this->count) ? $this->count : null;
+ }
+
+ public function concludesOn(): ?DateTime {
+ return isset($this->until) ? DateTime::createFromInterface($this->until) : null;
+ }
+
+ public function daysOfWeek(): array {
+ return is_array($this->byDay) ? $this->byDay : [];
+ }
+
+ public function daysOfMonth(): array {
+ return is_array($this->byMonthDay) ? $this->byMonthDay : [];
+ }
+
+ public function daysOfYear(): array {
+ return is_array($this->byYearDay) ? $this->byYearDay : [];
+ }
+
+ public function weeksOfYear(): array {
+ return is_array($this->byWeekNo) ? $this->byWeekNo : [];
+ }
+
+ public function monthsOfYear(): array {
+ return is_array($this->byMonth) ? $this->byMonth : [];
+ }
+
+ public function isRelative(): bool {
+ return isset($this->bySetPos);
+ }
+
+ public function relativePosition(): array {
+ return is_array($this->bySetPos) ? $this->bySetPos : [];
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Export/ExportService.php b/apps/dav/lib/CalDAV/Export/ExportService.php
new file mode 100644
index 00000000000..552b9e2b675
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Export/ExportService.php
@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Export;
+
+use Generator;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\ServerVersion;
+use Sabre\VObject\Component;
+use Sabre\VObject\Writer;
+
+/**
+ * Calendar Export Service
+ */
+class ExportService {
+
+ public const FORMATS = ['ical', 'jcal', 'xcal'];
+ private string $systemVersion;
+
+ public function __construct(ServerVersion $serverVersion) {
+ $this->systemVersion = $serverVersion->getVersionString();
+ }
+
+ /**
+ * Generates serialized content stream for a calendar and objects based in selected format
+ *
+ * @return Generator<string>
+ */
+ public function export(ICalendarExport $calendar, CalendarExportOptions $options): Generator {
+ // output start of serialized content based on selected format
+ yield $this->exportStart($options->getFormat());
+ // iterate through each returned vCalendar entry
+ // extract each component except timezones, convert to appropriate format and output
+ // extract any timezones and save them but do not output
+ $timezones = [];
+ foreach ($calendar->export($options) as $entry) {
+ $consecutive = false;
+ foreach ($entry->getComponents() as $vComponent) {
+ if ($vComponent->name === 'VTIMEZONE') {
+ if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) {
+ $timezones[$vComponent->TZID->getValue()] = clone $vComponent;
+ }
+ } else {
+ yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
+ $consecutive = true;
+ }
+ }
+ }
+ // iterate through each saved vTimezone entry, convert to appropriate format and output
+ foreach ($timezones as $vComponent) {
+ yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
+ $consecutive = true;
+ }
+ // output end of serialized content based on selected format
+ yield $this->exportFinish($options->getFormat());
+ }
+
+ /**
+ * Generates serialized content start based on selected format
+ */
+ private function exportStart(string $format): string {
+ return match ($format) {
+ 'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar Export v' . $this->systemVersion . '\/\/EN"]],[',
+ 'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar Export v' . $this->systemVersion . '//EN</text></prodid></properties><components>',
+ default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar Export v" . $this->systemVersion . "//EN\n"
+ };
+ }
+
+ /**
+ * Generates serialized content end based on selected format
+ */
+ private function exportFinish(string $format): string {
+ return match ($format) {
+ 'jcal' => ']]',
+ 'xcal' => '</components></vcalendar></icalendar>',
+ default => "END:VCALENDAR\n"
+ };
+ }
+
+ /**
+ * Generates serialized content for a component based on selected format
+ */
+ private function exportObject(Component $vobject, string $format, bool $consecutive): string {
+ return match ($format) {
+ 'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject),
+ 'xcal' => $this->exportObjectXml($vobject),
+ default => Writer::write($vobject)
+ };
+ }
+
+ /**
+ * Generates serialized content for a component in xml format
+ */
+ private function exportObjectXml(Component $vobject): string {
+ $writer = new \Sabre\Xml\Writer();
+ $writer->openMemory();
+ $writer->setIndent(false);
+ $vobject->xmlSerialize($writer);
+ return $writer->outputMemory();
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
index 2148e6a1da2..c2c474a90fe 100644
--- a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
+++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2023 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\FreeBusy;
diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
index 627959c90f6..08dc10f7bf4 100644
--- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
+++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ICSExportPlugin;
@@ -35,18 +19,16 @@ use Sabre\VObject\Property\ICalendar\Duration;
* @package OCA\DAV\CalDAV\ICSExportPlugin
*/
class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin {
- private IConfig $config;
- private LoggerInterface $logger;
-
/** @var string */
private const DEFAULT_REFRESH_INTERVAL = 'PT4H';
/**
* ICSExportPlugin constructor.
*/
- public function __construct(IConfig $config, LoggerInterface $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/IRestorable.php b/apps/dav/lib/CalDAV/IRestorable.php
index fab73c43d3a..5850e0a5645 100644
--- a/apps/dav/lib/CalDAV/IRestorable.php
+++ b/apps/dav/lib/CalDAV/IRestorable.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
index 0afb11c2673..acf81638679 100644
--- a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
+++ b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Integration;
@@ -49,21 +33,16 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
*/
private const DELIMITER = '--';
- /** @var string */
- private $appId;
-
- /** @var string */
- private $calendarUri;
-
/**
* ExternalCalendar constructor.
*
* @param string $appId
* @param string $calendarUri
*/
- public function __construct(string $appId, string $calendarUri) {
- $this->appId = $appId;
- $this->calendarUri = $calendarUri;
+ public function __construct(
+ private string $appId,
+ private string $calendarUri,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
index c72112f06ba..40a8860dcb4 100644
--- a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
+++ b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Integration;
diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
index e92eae2d3f1..c8a7109abde 100644
--- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
+++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
@@ -1,40 +1,31 @@
<?php
+
/**
- * @copyright Copyright (c) 2018, Georg Ehrke.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\InvitationResponse;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Connector\Sabre\LockPlugin;
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\RootCollection;
+use OCA\Theming\ThemingDefaults;
+use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\VObject\ITip\Message;
@@ -47,21 +38,23 @@ class InvitationResponseServer {
*/
public function __construct(bool $public = true) {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
- $logger = \OC::$server->get(LoggerInterface::class);
- /** @var IEventDispatcher $dispatcher */
- $dispatcher = \OC::$server->query(IEventDispatcher::class);
+ $logger = Server::get(LoggerInterface::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
// Add maintenance plugin
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
+ $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav')));
// Set URL explicitly due to reverse-proxy situations
$this->server->httpRequest->setUrl($baseUri);
$this->server->setBaseUri($baseUri);
- $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
+ $this->server->addPlugin(new BlockLegacyClientPlugin(
+ Server::get(IConfig::class),
+ Server::get(ThemingDefaults::class),
+ ));
$this->server->addPlugin(new AnonymousOptionsPlugin());
// allow custom principal uri option
@@ -75,8 +68,8 @@ class InvitationResponseServer {
$event = new SabrePluginAuthInitEvent($this->server);
$dispatcher->dispatchTyped($event);
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger));
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
+ $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
+ $this->server->addPlugin(new LockPlugin());
$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// acl
@@ -89,13 +82,13 @@ class InvitationResponseServer {
// calendar plugins
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class)));
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class)));
$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
//$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin(
- \OC::$server->getConfig(),
- \OC::$server->getURLGenerator()
+ $this->server->addPlugin(new PublishPlugin(
+ Server::get(IConfig::class),
+ Server::get(IURLGenerator::class)
));
// wait with registering these until auth is handled and the filesystem is setup
@@ -103,7 +96,7 @@ class InvitationResponseServer {
// register plugins from apps
$pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ Server::get(IAppManager::class)
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$this->server->addPlugin($appPlugin);
diff --git a/apps/dav/lib/CalDAV/Outbox.php b/apps/dav/lib/CalDAV/Outbox.php
index eebb48e1294..608114d8093 100644
--- a/apps/dav/lib/CalDAV/Outbox.php
+++ b/apps/dav/lib/CalDAV/Outbox.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -33,9 +16,6 @@ use Sabre\CalDAV\Plugin as CalDAVPlugin;
*/
class Outbox extends \Sabre\CalDAV\Schedule\Outbox {
- /** @var IConfig */
- private $config;
-
/** @var null|bool */
private $disableFreeBusy = null;
@@ -45,9 +25,11 @@ class Outbox extends \Sabre\CalDAV\Schedule\Outbox {
* @param IConfig $config
* @param string $principalUri
*/
- public function __construct(IConfig $config, string $principalUri) {
+ public function __construct(
+ private IConfig $config,
+ string $principalUri,
+ ) {
parent::__construct($principalUri);
- $this->config = $config;
}
/**
diff --git a/apps/dav/lib/CalDAV/Plugin.php b/apps/dav/lib/CalDAV/Plugin.php
index 5b367c51053..24448ae71ab 100644
--- a/apps/dav/lib/CalDAV/Plugin.php
+++ b/apps/dav/lib/CalDAV/Plugin.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/Principal/Collection.php b/apps/dav/lib/CalDAV/Principal/Collection.php
index 27997741609..b76fde66464 100644
--- a/apps/dav/lib/CalDAV/Principal/Collection.php
+++ b/apps/dav/lib/CalDAV/Principal/Collection.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
- *
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Principal;
diff --git a/apps/dav/lib/CalDAV/Principal/User.php b/apps/dav/lib/CalDAV/Principal/User.php
index 904ecc32e89..047d83827ed 100644
--- a/apps/dav/lib/CalDAV/Principal/User.php
+++ b/apps/dav/lib/CalDAV/Principal/User.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
- *
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Principal;
diff --git a/apps/dav/lib/CalDAV/Proxy/Proxy.php b/apps/dav/lib/CalDAV/Proxy/Proxy.php
index 8bafe8cc3b3..ef1ad8c634f 100644
--- a/apps/dav/lib/CalDAV/Proxy/Proxy.php
+++ b/apps/dav/lib/CalDAV/Proxy/Proxy.php
@@ -3,29 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Proxy;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method string getOwnerId()
@@ -45,8 +29,8 @@ class Proxy extends Entity {
protected $permissions;
public function __construct() {
- $this->addType('ownerId', 'string');
- $this->addType('proxyId', 'string');
- $this->addType('permissions', 'int');
+ $this->addType('ownerId', Types::STRING);
+ $this->addType('proxyId', Types::STRING);
+ $this->addType('permissions', Types::INTEGER);
}
}
diff --git a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
index e48e283484c..3b9b9c3d9eb 100644
--- a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
+++ b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Proxy;
diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php
index 16c7f86917d..9af6e544165 100644
--- a/apps/dav/lib/CalDAV/PublicCalendar.php
+++ b/apps/dav/lib/CalDAV/PublicCalendar.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/PublicCalendarObject.php b/apps/dav/lib/CalDAV/PublicCalendarObject.php
index 69a5583d8f5..2ab40b94347 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarObject.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarObject.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/PublicCalendarRoot.php b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
index a652b6ef1e5..edfb9f8dccc 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarRoot.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -32,18 +14,6 @@ use Sabre\DAV\Collection;
class PublicCalendarRoot extends Collection {
- /** @var CalDavBackend */
- protected $caldavBackend;
-
- /** @var \OCP\IL10N */
- protected $l10n;
-
- /** @var \OCP\IConfig */
- protected $config;
-
- /** @var LoggerInterface */
- private $logger;
-
/**
* PublicCalendarRoot constructor.
*
@@ -51,12 +21,12 @@ class PublicCalendarRoot extends Collection {
* @param IL10N $l10n
* @param IConfig $config
*/
- public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
- IConfig $config, LoggerInterface $logger) {
- $this->caldavBackend = $caldavBackend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
+ protected IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
index fedd918e6a4..76378e7a1c5 100644
--- a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
+++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
@@ -1,34 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Publishing;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IURLGenerator;
use Sabre\CalDAV\Xml\Property\AllowedSharingModes;
@@ -51,28 +31,21 @@ class PublishPlugin extends ServerPlugin {
protected $server;
/**
- * Config instance to get instance secret.
- *
- * @var IConfig
- */
- protected $config;
-
- /**
- * URL Generator for absolute URLs.
- *
- * @var IURLGenerator
- */
- protected $urlGenerator;
-
- /**
* PublishPlugin constructor.
*
* @param IConfig $config
* @param IURLGenerator $urlGenerator
*/
- public function __construct(IConfig $config, IURLGenerator $urlGenerator) {
- $this->config = $config;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ /**
+ * Config instance to get instance secret.
+ */
+ protected IConfig $config,
+ /**
+ * URL Generator for absolute URLs.
+ */
+ protected IURLGenerator $urlGenerator,
+ ) {
}
/**
@@ -119,17 +92,17 @@ class PublishPlugin extends ServerPlugin {
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof Calendar) {
- $propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) {
+ $propFind->handle('{' . self::NS_CALENDARSERVER . '}publish-url', function () use ($node) {
if ($node->getPublishStatus()) {
// We return the publish-url only if the calendar is published.
$token = $node->getPublishStatus();
- $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token;
+ $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri() . 'public-calendars/') . $token;
return new Publisher($publishUrl, true);
}
});
- $propFind->handle('{'.self::NS_CALENDARSERVER.'}allowed-sharing-modes', function () use ($node) {
+ $propFind->handle('{' . self::NS_CALENDARSERVER . '}allowed-sharing-modes', function () use ($node) {
$canShare = (!$node->isSubscription() && $node->canWrite());
$canPublish = (!$node->isSubscription() && $node->canWrite());
@@ -155,7 +128,7 @@ class PublishPlugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = (string) $request->getHeader('Content-Type');
+ $contentType = (string)$request->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -182,7 +155,7 @@ class PublishPlugin extends ServerPlugin {
switch ($documentType) {
- case '{'.self::NS_CALENDARSERVER.'}publish-calendar':
+ case '{' . self::NS_CALENDARSERVER . '}publish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
@@ -208,7 +181,7 @@ class PublishPlugin extends ServerPlugin {
$node->setPublishStatus(true);
// iCloud sends back the 202, so we will too.
- $response->setStatus(202);
+ $response->setStatus(Http::STATUS_ACCEPTED);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
@@ -217,7 +190,7 @@ class PublishPlugin extends ServerPlugin {
// Breaking the event chain
return false;
- case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar':
+ case '{' . self::NS_CALENDARSERVER . '}unpublish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
@@ -242,7 +215,7 @@ class PublishPlugin extends ServerPlugin {
$node->setPublishStatus(false);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
index cbff5e66a85..fb9b7298f9b 100644
--- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
+++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Publishing\Xml;
@@ -29,22 +12,13 @@ use Sabre\Xml\XmlSerializable;
class Publisher implements XmlSerializable {
/**
- * @var string $publishUrl
- */
- protected $publishUrl;
-
- /**
- * @var boolean $isPublished
- */
- protected $isPublished;
-
- /**
* @param string $publishUrl
* @param boolean $isPublished
*/
- public function __construct($publishUrl, $isPublished) {
- $this->publishUrl = $publishUrl;
- $this->isPublished = $isPublished;
+ public function __construct(
+ protected $publishUrl,
+ protected $isPublished,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php
index f1f5d8c4ac3..329af3a2f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Backend.php
+++ b/apps/dav/lib/CalDAV/Reminder/Backend.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -38,22 +18,16 @@ use OCP\IDBConnection;
*/
class Backend {
- /** @var IDBConnection */
- protected $db;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
/**
* Backend constructor.
*
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
*/
- public function __construct(IDBConnection $db,
- ITimeFactory $timeFactory) {
- $this->db = $db;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ protected IDBConnection $db,
+ protected ITimeFactory $timeFactory,
+ ) {
}
/**
@@ -64,12 +38,13 @@ class Backend {
*/
public function getRemindersToProcess():array {
$query = $this->db->getQueryBuilder();
- $query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri'])
+ $query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri'])
->from('calendar_reminders', 'cr')
->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime())))
->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id'))
- ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'));
- $stmt = $query->execute();
+ ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'))
+ ->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri');
+ $stmt = $query->executeQuery();
return array_map(
[$this, 'fixRowTyping'],
@@ -88,7 +63,7 @@ class Backend {
$query->select('*')
->from('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
- $stmt = $query->execute();
+ $stmt = $query->executeQuery();
return array_map(
[$this, 'fixRowTyping'],
@@ -141,7 +116,7 @@ class Backend {
'notification_date' => $query->createNamedParameter($notificationDate),
'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0),
])
- ->execute();
+ ->executeStatement();
return $query->getLastInsertId();
}
@@ -158,7 +133,7 @@ class Backend {
$query->update('calendar_reminders')
->set('notification_date', $query->createNamedParameter($newNotificationDate))
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -172,7 +147,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -185,7 +160,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -199,7 +174,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -207,15 +182,15 @@ class Backend {
* @return array
*/
private function fixRowTyping(array $row): array {
- $row['id'] = (int) $row['id'];
- $row['calendar_id'] = (int) $row['calendar_id'];
- $row['object_id'] = (int) $row['object_id'];
- $row['is_recurring'] = (bool) $row['is_recurring'];
- $row['recurrence_id'] = (int) $row['recurrence_id'];
- $row['is_recurrence_exception'] = (bool) $row['is_recurrence_exception'];
- $row['is_relative'] = (bool) $row['is_relative'];
- $row['notification_date'] = (int) $row['notification_date'];
- $row['is_repeat_based'] = (bool) $row['is_repeat_based'];
+ $row['id'] = (int)$row['id'];
+ $row['calendar_id'] = (int)$row['calendar_id'];
+ $row['object_id'] = (int)$row['object_id'];
+ $row['is_recurring'] = (bool)$row['is_recurring'];
+ $row['recurrence_id'] = (int)$row['recurrence_id'];
+ $row['is_recurrence_exception'] = (bool)$row['is_recurrence_exception'];
+ $row['is_relative'] = (bool)$row['is_relative'];
+ $row['notification_date'] = (int)$row['notification_date'];
+ $row['is_repeat_based'] = (bool)$row['is_repeat_based'];
return $row;
}
diff --git a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
index 1eb3932e611..31d60f1531d 100644
--- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -49,6 +29,6 @@ interface INotificationProvider {
*/
public function send(VEvent $vevent,
?string $calendarDisplayName,
- array $principalEmailAddresses,
+ array $principalEmailAddresses,
array $users = []): void;
}
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
index 52c994554cc..94edff98e52 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
@@ -3,30 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
@@ -51,31 +29,18 @@ abstract class AbstractProvider implements INotificationProvider {
/** @var string */
public const NOTIFICATION_TYPE = '';
- protected LoggerInterface $logger;
-
- /** @var L10NFactory */
- protected $l10nFactory;
-
/** @var IL10N[] */
private $l10ns;
/** @var string */
private $fallbackLanguage;
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var IConfig */
- protected $config;
-
- public function __construct(LoggerInterface $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- IConfig $config) {
- $this->logger = $logger;
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->config = $config;
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected L10NFactory $l10nFactory,
+ protected IURLGenerator $urlGenerator,
+ protected IConfig $config,
+ ) {
}
/**
@@ -135,7 +100,7 @@ abstract class AbstractProvider implements INotificationProvider {
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
- return (string) $vevent->STATUS;
+ return (string)$vevent->STATUS;
}
// Doesn't say so in the standard,
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
index 4b369b34dc0..01d51489a3b 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
index 262ceb479f0..0fd39a9e459 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
@@ -3,31 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
@@ -40,6 +17,7 @@ use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\Headers\AutoSubmitted;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
+use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VEvent;
@@ -55,15 +33,14 @@ class EmailProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'EMAIL';
- private IMailer $mailer;
-
- public function __construct(IConfig $config,
- IMailer $mailer,
+ public function __construct(
+ IConfig $config,
+ private IMailer $mailer,
LoggerInterface $logger,
L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator) {
+ IURLGenerator $urlGenerator,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->mailer = $mailer;
}
/**
@@ -110,7 +87,7 @@ class EmailProvider extends AbstractProvider {
$lang = $fallbackLanguage;
}
$l10n = $this->getL10NForLang($lang);
- $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
+ $fromEMail = Util::getDefaultEmailAddress('reminders-noreply');
$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
$template->addHeader();
@@ -172,11 +149,11 @@ class EmailProvider extends AbstractProvider {
$this->getAbsoluteImagePath('places/calendar.png'));
if (isset($vevent->LOCATION)) {
- $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
+ $template->addBodyListItem((string)$vevent->LOCATION, $l10n->t('Where:'),
$this->getAbsoluteImagePath('actions/address.png'));
}
if (isset($vevent->DESCRIPTION)) {
- $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
+ $template->addBodyListItem((string)$vevent->DESCRIPTION, $l10n->t('Description:'),
$this->getAbsoluteImagePath('actions/more.png'));
}
}
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
index 2e4f9a38493..15994bacf49 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
index 79e4e44e6d8..a3f0cce547a 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
@@ -3,30 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
@@ -52,21 +30,15 @@ class PushProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'DISPLAY';
- /** @var IManager */
- private $manager;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- public function __construct(IConfig $config,
- IManager $manager,
+ public function __construct(
+ IConfig $config,
+ private IManager $manager,
LoggerInterface $logger,
L10NFactory $l10nFactory,
IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
+ private ITimeFactory $timeFactory,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->manager = $manager;
- $this->timeFactory = $timeFactory;
}
/**
@@ -87,7 +59,7 @@ class PushProvider extends AbstractProvider {
}
$eventDetails = $this->extractEventDetails($vevent);
- $eventUUID = (string) $vevent->UID;
+ $eventUUID = (string)$vevent->UID;
if (!$eventUUID) {
return;
};
@@ -122,13 +94,13 @@ class PushProvider extends AbstractProvider {
return [
'title' => isset($vevent->SUMMARY)
- ? ((string) $vevent->SUMMARY)
+ ? ((string)$vevent->SUMMARY)
: null,
'description' => isset($vevent->DESCRIPTION)
- ? ((string) $vevent->DESCRIPTION)
+ ? ((string)$vevent->DESCRIPTION)
: null,
'location' => isset($vevent->LOCATION)
- ? ((string) $vevent->LOCATION)
+ ? ((string)$vevent->LOCATION)
: null,
'all_day' => $start instanceof Property\ICalendar\Date,
'start_atom' => $start->getDateTime()->format(\DateTimeInterface::ATOM),
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
index cd8030a1177..265db09b061 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
@@ -3,30 +3,15 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException;
+use OCP\AppFramework\QueryException;
+use OCP\Server;
+
/**
* Class NotificationProviderManager
*
@@ -61,7 +46,7 @@ class NotificationProviderManager {
if (isset($this->providers[$type])) {
return $this->providers[$type];
}
- throw new NotificationProvider\ProviderNotAvailableException($type);
+ throw new ProviderNotAvailableException($type);
}
throw new NotificationTypeDoesNotExistException($type);
}
@@ -70,10 +55,10 @@ class NotificationProviderManager {
* Registers a new provider
*
* @param string $providerClassName
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
*/
public function registerProvider(string $providerClassName):void {
- $provider = \OC::$server->query($providerClassName);
+ $provider = Server::get($providerClassName);
if (!$provider instanceof INotificationProvider) {
throw new \InvalidArgumentException('Invalid notification provider registered');
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
index 16fb858bc3a..6fd2a29ede5 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php
index e8f0405f3ce..137fb509f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Notifier.php
+++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -38,6 +17,7 @@ use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
+use OCP\Notification\UnknownNotificationException;
/**
* Class Notifier
@@ -46,31 +26,21 @@ use OCP\Notification\INotifier;
*/
class Notifier implements INotifier {
- /** @var IFactory */
- private $l10nFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
/** @var IL10N */
private $l10n;
- /** @var ITimeFactory */
- private $timeFactory;
-
/**
* Notifier constructor.
*
- * @param IFactory $factory
+ * @param IFactory $l10nFactory
* @param IURLGenerator $urlGenerator
* @param ITimeFactory $timeFactory
*/
- public function __construct(IFactory $factory,
- IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
- $this->l10nFactory = $factory;
- $this->urlGenerator = $urlGenerator;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ private IFactory $l10nFactory,
+ private IURLGenerator $urlGenerator,
+ private ITimeFactory $timeFactory,
+ ) {
}
/**
@@ -99,12 +69,12 @@ class Notifier implements INotifier {
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
- * @throws \Exception
+ * @throws UnknownNotificationException
*/
public function prepare(INotification $notification,
string $languageCode):INotification {
if ($notification->getApp() !== Application::APP_ID) {
- throw new \InvalidArgumentException('Notification not from this app');
+ throw new UnknownNotificationException('Notification not from this app');
}
// Read the language from the notification
@@ -116,7 +86,7 @@ class Notifier implements INotifier {
return $this->prepareReminderNotification($notification);
default:
- throw new \InvalidArgumentException('Unknown subject');
+ throw new UnknownNotificationException('Unknown subject');
}
}
diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
index 9f4b55824e8..c75090e1560 100644
--- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php
+++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
@@ -3,31 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -55,33 +32,6 @@ use function strcasecmp;
class ReminderService {
- /** @var Backend */
- private $backend;
-
- /** @var NotificationProviderManager */
- private $notificationProviderManager;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var CalDavBackend */
- private $caldavBackend;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
- /** @var Principal */
- private $principalConnector;
-
public const REMINDER_TYPE_EMAIL = 'EMAIL';
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
public const REMINDER_TYPE_AUDIO = 'AUDIO';
@@ -97,24 +47,17 @@ class ReminderService {
self::REMINDER_TYPE_AUDIO
];
- public function __construct(Backend $backend,
- NotificationProviderManager $notificationProviderManager,
- IUserManager $userManager,
- IGroupManager $groupManager,
- CalDavBackend $caldavBackend,
- ITimeFactory $timeFactory,
- IConfig $config,
- LoggerInterface $logger,
- Principal $principalConnector) {
- $this->backend = $backend;
- $this->notificationProviderManager = $notificationProviderManager;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->caldavBackend = $caldavBackend;
- $this->timeFactory = $timeFactory;
- $this->config = $config;
- $this->logger = $logger;
- $this->principalConnector = $principalConnector;
+ public function __construct(
+ private Backend $backend,
+ private NotificationProviderManager $notificationProviderManager,
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private CalDavBackend $caldavBackend,
+ private ITimeFactory $timeFactory,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private Principal $principalConnector,
+ ) {
}
/**
@@ -229,14 +172,14 @@ class ReminderService {
if (!$vcalendar) {
return;
}
- $calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$objectData['calendarid']);
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
@@ -306,7 +249,7 @@ class ReminderService {
continue;
}
- if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
+ if (!\in_array((string)$valarm->ACTION, self::REMINDER_TYPES, true)) {
// Action allows x-name, we don't insert reminders
// into the database if they are not standard
$processedAlarms[] = $alarmHash;
@@ -376,7 +319,7 @@ class ReminderService {
return;
}
- $this->backend->cleanRemindersForEvent((int) $objectData['id']);
+ $this->backend->cleanRemindersForEvent((int)$objectData['id']);
}
/**
@@ -425,19 +368,19 @@ class ReminderService {
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
- 'uid' => (string) $valarm->parent->UID,
+ 'uid' => (string)$valarm->parent->UID,
'is_recurring' => $isRecurring,
'recurrence_id' => $recurrenceId,
'is_recurrence_exception' => $isRecurrenceException,
'event_hash' => $eventHash,
'alarm_hash' => $alarmHash,
- 'type' => (string) $valarm->ACTION,
+ 'type' => (string)$valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $notificationDate->getTimestamp(),
'is_repeat_based' => false,
];
- $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
+ $repeat = isset($valarm->REPEAT) ? (int)$valarm->REPEAT->getValue() : 0;
for ($i = 0; $i < $repeat; $i++) {
if ($valarm->DURATION === null) {
continue;
@@ -447,13 +390,13 @@ class ReminderService {
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
- 'uid' => (string) $valarm->parent->UID,
+ 'uid' => (string)$valarm->parent->UID,
'is_recurring' => $isRecurring,
'recurrence_id' => $recurrenceId,
'is_recurrence_exception' => $isRecurrenceException,
'event_hash' => $eventHash,
'alarm_hash' => $alarmHash,
- 'type' => (string) $valarm->ACTION,
+ 'type' => (string)$valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $clonedNotificationDate->getTimestamp(),
'is_repeat_based' => true,
@@ -467,19 +410,26 @@ class ReminderService {
* @param array $reminders
*/
private function writeRemindersToDatabase(array $reminders): void {
+ $uniqueReminders = [];
foreach ($reminders as $reminder) {
+ $key = $reminder['notification_date'] . $reminder['event_hash'] . $reminder['type'];
+ if (!isset($uniqueReminders[$key])) {
+ $uniqueReminders[$key] = $reminder;
+ }
+ }
+ foreach (array_values($uniqueReminders) as $reminder) {
$this->backend->insertReminder(
- (int) $reminder['calendar_id'],
- (int) $reminder['object_id'],
+ (int)$reminder['calendar_id'],
+ (int)$reminder['object_id'],
$reminder['uid'],
$reminder['is_recurring'],
- (int) $reminder['recurrence_id'],
+ (int)$reminder['recurrence_id'],
$reminder['is_recurrence_exception'],
$reminder['event_hash'],
$reminder['alarm_hash'],
$reminder['type'],
$reminder['is_relative'],
- (int) $reminder['notification_date'],
+ (int)$reminder['notification_date'],
$reminder['is_repeat_based']
);
}
@@ -491,10 +441,10 @@ class ReminderService {
*/
private function deleteOrProcessNext(array $reminder,
VObject\Component\VEvent $vevent):void {
- if ($reminder['is_repeat_based'] ||
- !$reminder['is_recurring'] ||
- !$reminder['is_relative'] ||
- $reminder['is_recurrence_exception']) {
+ if ($reminder['is_repeat_based']
+ || !$reminder['is_recurring']
+ || !$reminder['is_relative']
+ || $reminder['is_recurrence_exception']) {
$this->backend->removeReminder($reminder['id']);
return;
}
@@ -502,7 +452,7 @@ class ReminderService {
$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
- $calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']);
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']);
try {
$iterator = new EventIterator($vevents, $reminder['uid']);
@@ -618,26 +568,26 @@ class ReminderService {
*/
private function getEventHash(VEvent $vevent):string {
$properties = [
- (string) $vevent->DTSTART->serialize(),
+ (string)$vevent->DTSTART->serialize(),
];
if ($vevent->DTEND) {
- $properties[] = (string) $vevent->DTEND->serialize();
+ $properties[] = (string)$vevent->DTEND->serialize();
}
if ($vevent->DURATION) {
- $properties[] = (string) $vevent->DURATION->serialize();
+ $properties[] = (string)$vevent->DURATION->serialize();
}
if ($vevent->{'RECURRENCE-ID'}) {
- $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
+ $properties[] = (string)$vevent->{'RECURRENCE-ID'}->serialize();
}
if ($vevent->RRULE) {
- $properties[] = (string) $vevent->RRULE->serialize();
+ $properties[] = (string)$vevent->RRULE->serialize();
}
if ($vevent->EXDATE) {
- $properties[] = (string) $vevent->EXDATE->serialize();
+ $properties[] = (string)$vevent->EXDATE->serialize();
}
if ($vevent->RDATE) {
- $properties[] = (string) $vevent->RDATE->serialize();
+ $properties[] = (string)$vevent->RDATE->serialize();
}
return md5(implode('::', $properties));
@@ -652,15 +602,15 @@ class ReminderService {
*/
private function getAlarmHash(VAlarm $valarm):string {
$properties = [
- (string) $valarm->ACTION->serialize(),
- (string) $valarm->TRIGGER->serialize(),
+ (string)$valarm->ACTION->serialize(),
+ (string)$valarm->TRIGGER->serialize(),
];
if ($valarm->DURATION) {
- $properties[] = (string) $valarm->DURATION->serialize();
+ $properties[] = (string)$valarm->DURATION->serialize();
}
if ($valarm->REPEAT) {
- $properties[] = (string) $valarm->REPEAT->serialize();
+ $properties[] = (string)$valarm->REPEAT->serialize();
}
return md5(implode('::', $properties));
@@ -680,7 +630,7 @@ class ReminderService {
return null;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
@@ -731,7 +681,7 @@ class ReminderService {
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
- return (string) $vevent->STATUS;
+ return (string)$vevent->STATUS;
}
// Doesn't say so in the standard,
@@ -870,7 +820,7 @@ class ReminderService {
private function getCalendarTimeZone(int $calendarid): DateTimeZone {
$calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
$tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
- if (!isset($calendarInfo[$tzProp])) {
+ if (empty($calendarInfo[$tzProp])) {
// Defaulting to UTC
return new DateTimeZone('UTC');
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
index 92c61e72780..68bb3373346 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
@@ -45,23 +26,6 @@ use function array_values;
abstract class AbstractPrincipalBackend implements BackendInterface {
- /** @var IDBConnection */
- private $db;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IGroupManager */
- private $groupManager;
-
- private LoggerInterface $logger;
-
- /** @var ProxyMapper */
- private $proxyMapper;
-
- /** @var string */
- private $principalPrefix;
-
/** @var string */
private $dbTableName;
@@ -71,27 +35,19 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
/** @var string */
private $dbForeignKeyName;
- /** @var string */
- private $cuType;
-
- public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- LoggerInterface $logger,
- ProxyMapper $proxyMapper,
- string $principalPrefix,
+ public function __construct(
+ private IDBConnection $db,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private LoggerInterface $logger,
+ private ProxyMapper $proxyMapper,
+ private string $principalPrefix,
string $dbPrefix,
- string $cuType) {
- $this->db = $dbConnection;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->logger = $logger;
- $this->proxyMapper = $proxyMapper;
- $this->principalPrefix = $principalPrefix;
+ private string $cuType,
+ ) {
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
$this->dbMetaDataTableName = $this->dbTableName . '_md';
$this->dbForeignKeyName = $dbPrefix . '_id';
- $this->cuType = $cuType;
}
use PrincipalProxyTrait;
@@ -130,8 +86,8 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
$metaDataById[$metaDataRow[$this->dbForeignKeyName]] = [];
}
- $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] =
- $metaDataRow['value'];
+ $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']]
+ = $metaDataRow['value'];
}
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
@@ -406,7 +362,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
try {
$stmt = $query->executeQuery();
} catch (Exception $e) {
- $this->logger->error("Could not search resources: " . $e->getMessage(), ['exception' => $e]);
+ $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]);
}
$rows = [];
@@ -515,9 +471,9 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
* @return bool
*/
private function isAllowedToAccessResource(array $row, array $userGroups): bool {
- if (!isset($row['group_restrictions']) ||
- $row['group_restrictions'] === null ||
- $row['group_restrictions'] === '') {
+ if (!isset($row['group_restrictions'])
+ || $row['group_restrictions'] === null
+ || $row['group_restrictions'] === '') {
return true;
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
index 45795377e11..c70d93daf52 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
index 829ca7d6033..5704b23ae14 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php
index 9a43d5bdc91..399d1a46639 100644
--- a/apps/dav/lib/CalDAV/RetentionService.php
+++ b/apps/dav/lib/CalDAV/RetentionService.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -34,29 +17,19 @@ class RetentionService {
public const RETENTION_CONFIG_KEY = 'calendarRetentionObligation';
private const DEFAULT_RETENTION_SECONDS = 30 * 24 * 60 * 60;
- /** @var IConfig */
- private $config;
-
- /** @var ITimeFactory */
- private $time;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- public function __construct(IConfig $config,
- ITimeFactory $time,
- CalDavBackend $calDavBackend) {
- $this->config = $config;
- $this->time = $time;
- $this->calDavBackend = $calDavBackend;
+ public function __construct(
+ private IConfig $config,
+ private ITimeFactory $time,
+ private CalDavBackend $calDavBackend,
+ ) {
}
public function getDuration(): int {
return max(
- (int) $this->config->getAppValue(
+ (int)$this->config->getAppValue(
Application::APP_ID,
self::RETENTION_CONFIG_KEY,
- (string) self::DEFAULT_RETENTION_SECONDS
+ (string)self::DEFAULT_RETENTION_SECONDS
),
0 // Just making sure we don't delete things in the future when a negative number is passed
);
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
index fcc2ae1e166..2af6b162d8d 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -1,38 +1,10 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
- * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
- * @copyright 2022 Anna Larch <anna.larch@gmx.net>
- *
- * @author brad2014 <brad2014@users.noreply.github.com>
- * @author Brad Rubenstein <brad@wbr.tech>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Leon Klingele <leon@struktur.de>
- * @author Nick Sweeting <git@sweeting.me>
- * @author rakekniven <mark.ziegler@rakekniven.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2007-2015 fruux GmbH (https://fruux.com/)
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV\Schedule;
@@ -40,9 +12,13 @@ use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\EventComparisonService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
-use OCP\IConfig;
-use OCP\IUserManager;
+use OCP\IAppConfig;
+use OCP\IUserSession;
use OCP\Mail\IMailer;
+use OCP\Mail\Provider\Address;
+use OCP\Mail\Provider\Attachment;
+use OCP\Mail\Provider\IManager as IMailManager;
+use OCP\Mail\Provider\IMessageSend;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
@@ -69,41 +45,26 @@ use Sabre\VObject\Reader;
* @license http://sabre.io/license/ Modified BSD License
*/
class IMipPlugin extends SabreIMipPlugin {
- private ?string $userId;
- private IConfig $config;
- private IMailer $mailer;
- private LoggerInterface $logger;
- private ITimeFactory $timeFactory;
- private Defaults $defaults;
- private IUserManager $userManager;
+
private ?VCalendar $vCalendar = null;
- private IMipService $imipService;
public const MAX_DATE = '2038-01-01';
public const METHOD_REQUEST = 'request';
public const METHOD_REPLY = 'reply';
public const METHOD_CANCEL = 'cancel';
- public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
- private EventComparisonService $eventComparisonService;
-
- public function __construct(IConfig $config,
- IMailer $mailer,
- LoggerInterface $logger,
- ITimeFactory $timeFactory,
- Defaults $defaults,
- IUserManager $userManager,
- $userId,
- IMipService $imipService,
- EventComparisonService $eventComparisonService) {
+ public const IMIP_INDENT = 15;
+
+ public function __construct(
+ private IAppConfig $config,
+ private IMailer $mailer,
+ private LoggerInterface $logger,
+ private ITimeFactory $timeFactory,
+ private Defaults $defaults,
+ private IUserSession $userSession,
+ private IMipService $imipService,
+ private EventComparisonService $eventComparisonService,
+ private IMailManager $mailManager,
+ ) {
parent::__construct('');
- $this->userId = $userId;
- $this->config = $config;
- $this->mailer = $mailer;
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
- $this->defaults = $defaults;
- $this->userManager = $userManager;
- $this->imipService = $imipService;
- $this->eventComparisonService = $eventComparisonService;
}
public function initialize(DAV\Server $server): void {
@@ -120,7 +81,7 @@ class IMipPlugin extends SabreIMipPlugin {
* @param bool $modified modified
*/
public function beforeWriteContent($uri, INode $node, $data, $modified): void {
- if(!$node instanceof CalendarObject) {
+ if (!$node instanceof CalendarObject) {
return;
}
/** @var VCalendar $vCalendar */
@@ -135,8 +96,8 @@ class IMipPlugin extends SabreIMipPlugin {
* @return void
*/
public function schedule(Message $iTipMessage) {
- // Not sending any emails if the system considers the update
- // insignificant.
+
+ // Not sending any emails if the system considers the update insignificant
if (!$iTipMessage->significantChange) {
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
@@ -177,7 +138,7 @@ class IMipPlugin extends SabreIMipPlugin {
// No changed events after all - this shouldn't happen if there is significant change yet here we are
// The scheduling status is debatable
- if(empty($vEvent)) {
+ if (empty($vEvent)) {
$this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents');
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
return;
@@ -189,15 +150,16 @@ class IMipPlugin extends SabreIMipPlugin {
// we also might not have an old event as this could be a new
// invitation, or a new recurrence exception
$attendee = $this->imipService->getCurrentAttendee($iTipMessage);
- if($attendee === null) {
+ if ($attendee === null) {
$uid = $vEvent->UID ?? 'no UID found';
$this->logger->debug('Could not find recipient ' . $recipient . ' as attendee for event with UID ' . $uid);
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
- // Don't send emails to things
- if($this->imipService->isRoomOrResource($attendee)) {
- $this->logger->debug('No invitation sent as recipient is room or resource', [
+ // Don't send emails to rooms, resources and circles
+ if ($this->imipService->isRoomOrResource($attendee)
+ || $this->imipService->isCircle($attendee)) {
+ $this->logger->debug('No invitation sent as recipient is room, resource or circle', [
'attendee' => $recipient,
]);
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
@@ -206,17 +168,16 @@ class IMipPlugin extends SabreIMipPlugin {
$this->imipService->setL10n($attendee);
// Build the sender name.
- // Due to a bug in sabre, the senderName property for an iTIP message
- // can actually also be a VObject Property
- /** @var Parameter|string|null $senderName */
- $senderName = $iTipMessage->senderName ?: null;
- if($senderName instanceof Parameter) {
- $senderName = $senderName->getValue() ?? null;
- }
-
- // Try to get the sender name from the current user id if available.
- if ($this->userId !== null && ($senderName === null || empty(trim($senderName)))) {
- $senderName = $this->userManager->getDisplayName($this->userId);
+ // Due to a bug in sabre, the senderName property for an iTIP message can actually also be a VObject Property
+ // If the iTIP message senderName is null or empty use the user session name as the senderName
+ if (($iTipMessage->senderName instanceof Parameter) && !empty(trim($iTipMessage->senderName->getValue()))) {
+ $senderName = trim($iTipMessage->senderName->getValue());
+ } elseif (is_string($iTipMessage->senderName) && !empty(trim($iTipMessage->senderName))) {
+ $senderName = trim($iTipMessage->senderName);
+ } elseif ($this->userSession->getUser() !== null) {
+ $senderName = trim($this->userSession->getUser()->getDisplayName());
+ } else {
+ $senderName = '';
}
$sender = substr($iTipMessage->sender, 7);
@@ -225,7 +186,7 @@ class IMipPlugin extends SabreIMipPlugin {
switch (strtolower($iTipMessage->method)) {
case self::METHOD_REPLY:
$method = self::METHOD_REPLY;
- $data = $this->imipService->buildBodyData($vEvent, $oldVevent);
+ $data = $this->imipService->buildReplyBodyData($vEvent);
$replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
@@ -244,21 +205,6 @@ class IMipPlugin extends SabreIMipPlugin {
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
- $message = $this->mailer->createMessage()
- ->setFrom([$fromEMail => $fromName]);
-
- if ($recipientName !== null) {
- $message->setTo([$recipient => $recipientName]);
- } else {
- $message->setTo([$recipient]);
- }
-
- if ($senderName !== null) {
- $message->setReplyTo([$sender => $senderName]);
- } else {
- $message->setReplyTo([$sender]);
- }
-
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->addHeader();
@@ -288,7 +234,7 @@ class IMipPlugin extends SabreIMipPlugin {
*/
$recipientDomain = substr(strrchr($recipient, '@'), 1);
- $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
+ $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getValueString('dav', 'invitation_link_recipients', 'yes'))));
if (strcmp('yes', $invitationLinkRecipients[0]) === 0
|| in_array(strtolower($recipient), $invitationLinkRecipients)
@@ -300,18 +246,70 @@ class IMipPlugin extends SabreIMipPlugin {
}
$template->addFooter();
-
- $message->useTemplate($template);
-
+ // convert iTip Message to string
$itip_msg = $iTipMessage->message->serialize();
- $message->attachInline(
- $itip_msg,
- 'event.ics',
- 'text/calendar; method=' . $iTipMessage->method,
- );
+
+ $mailService = null;
try {
- $failed = $this->mailer->send($message);
+ if ($this->config->getValueBool('core', 'mail_providers_enabled', true)) {
+ // retrieve user object
+ $user = $this->userSession->getUser();
+ if ($user !== null) {
+ // retrieve appropriate service with the same address as sender
+ $mailService = $this->mailManager->findServiceByAddress($user->getUID(), $sender);
+ }
+ }
+
+ // The display name in Nextcloud can use utf-8.
+ // As the default charset for text/* is us-ascii, it's important to explicitly define it.
+ // See https://www.rfc-editor.org/rfc/rfc6047.html#section-2.4.
+ $contentType = 'text/calendar; method=' . $iTipMessage->method . '; charset="utf-8"';
+
+ // evaluate if a mail service was found and has sending capabilities
+ if ($mailService instanceof IMessageSend) {
+ // construct mail message and set required parameters
+ $message = $mailService->initiateMessage();
+ $message->setFrom(
+ (new Address($sender, $fromName))
+ );
+ $message->setTo(
+ (new Address($recipient, $recipientName))
+ );
+ $message->setSubject($template->renderSubject());
+ $message->setBodyPlain($template->renderText());
+ $message->setBodyHtml($template->renderHtml());
+ // Adding name=event.ics is a trick to make the invitation also appear
+ // as a file attachment in mail clients like Thunderbird or Evolution.
+ $message->setAttachments((new Attachment(
+ $itip_msg,
+ null,
+ $contentType . '; name=event.ics',
+ true
+ )));
+ // send message
+ $mailService->sendMessage($message);
+ } else {
+ // construct symfony mailer message and set required parameters
+ $message = $this->mailer->createMessage();
+ $message->setFrom([$fromEMail => $fromName]);
+ $message->setTo(
+ (($recipientName !== null) ? [$recipient => $recipientName] : [$recipient])
+ );
+ $message->setReplyTo(
+ (($senderName !== null) ? [$sender => $senderName] : [$sender])
+ );
+ $message->useTemplate($template);
+ // Using a different content type because Symfony Mailer/Mime will append the name to
+ // the content type header and attachInline does not allow null.
+ $message->attachInline(
+ $itip_msg,
+ 'event.ics',
+ $contentType,
+ );
+ $failed = $this->mailer->send($message);
+ }
+
$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
if (!empty($failed)) {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
index 4cd859d79ac..54c0bc31849 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipService.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -1,31 +1,16 @@
<?php
declare(strict_types=1);
-/*
- * DAV App
- *
- * @copyright 2022 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Schedule;
use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
@@ -42,11 +27,6 @@ use Sabre\VObject\Recur\EventIterator;
class IMipService {
- private URLGenerator $urlGenerator;
- private IConfig $config;
- private IDBConnection $db;
- private ISecureRandom $random;
- private L10NFactory $l10nFactory;
private IL10N $l10n;
/** @var string[] */
@@ -57,18 +37,17 @@ class IMipService {
'meeting_location' => 'LOCATION'
];
- public function __construct(URLGenerator $urlGenerator,
- IConfig $config,
- IDBConnection $db,
- ISecureRandom $random,
- L10NFactory $l10nFactory) {
- $this->urlGenerator = $urlGenerator;
- $this->config = $config;
- $this->db = $db;
- $this->random = $random;
- $this->l10nFactory = $l10nFactory;
- $default = $this->l10nFactory->findGenericLanguage();
- $this->l10n = $this->l10nFactory->get('dav', $default);
+ public function __construct(
+ private URLGenerator $urlGenerator,
+ private IConfig $config,
+ private IDBConnection $db,
+ private ISecureRandom $random,
+ private L10NFactory $l10nFactory,
+ private ITimeFactory $timeFactory,
+ ) {
+ $language = $this->l10nFactory->findGenericLanguage();
+ $locale = $this->l10nFactory->findLocale($language);
+ $this->l10n = $this->l10nFactory->get('dav', $language, $locale);
}
/**
@@ -100,7 +79,7 @@ class IMipService {
return $default;
}
$newstring = $vevent->$property->getValue();
- if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
+ if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
$oldstring = $oldVEvent->$property->getValue();
return sprintf($strikethrough, $oldstring, $newstring);
}
@@ -147,11 +126,15 @@ class IMipService {
* @return array
*/
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
+
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
+ $eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null;
$defaultVal = '';
$data = [];
- $data['meeting_when'] = $this->generateWhenString($vEvent);
+ $data['meeting_when'] = $this->generateWhenString($eventReaderCurrent);
- foreach(self::STRING_DIFF as $key => $property) {
+ foreach (self::STRING_DIFF as $key => $property) {
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
}
@@ -161,8 +144,8 @@ class IMipService {
$data['meeting_location_html'] = $locationHtml;
}
- if(!empty($oldVEvent)) {
- $oldMeetingWhen = $this->generateWhenString($oldVEvent);
+ if (!empty($oldVEvent)) {
+ $oldMeetingWhen = $this->generateWhenString($eventReaderPrevious);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
@@ -170,107 +153,635 @@ class IMipService {
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
- $data['meeting_when_html'] =
- ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
- ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
- : $data['meeting_when'];
+ $data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when'];
+ }
+ // generate occurring next string
+ if ($eventReaderCurrent->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent);
}
return $data;
}
/**
- * @param IL10N $this->l10n
- * @param VEvent $vevent
- * @return false|int|string
+ * @param VEvent $vEvent
+ * @return array
*/
- public function generateWhenString(VEvent $vevent) {
- /** @var Property\ICalendar\DateTime $dtstart */
- $dtstart = $vevent->DTSTART;
- if (isset($vevent->DTEND)) {
- /** @var Property\ICalendar\DateTime $dtend */
- $dtend = $vevent->DTEND;
- } elseif (isset($vevent->DURATION)) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
- $dtend->setDateTime($endDateTime, $isFloating);
- } elseif (!$dtstart->hasTime()) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->modify('+1 day');
- $dtend->setDateTime($endDateTime, $isFloating);
- } else {
- $dtend = clone $dtstart;
+ public function buildReplyBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReader = new EventReader($vEvent);
+ $defaultVal = '';
+ $data = [];
+ $data['meeting_when'] = $this->generateWhenString($eventReader);
+
+ foreach (self::STRING_DIFF as $key => $property) {
+ $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
}
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
- /** @var \DateTimeImmutable $dtstartDt */
- $dtstartDt = $dtstart->getDateTime();
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
- /** @var \DateTimeImmutable $dtendDt */
- $dtendDt = $dtend->getDateTime();
+ $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : '';
- $diff = $dtstartDt->diff($dtendDt);
+ // generate occurring next string
+ if ($eventReader->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReader);
+ }
- $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
- $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
+ return $data;
+ }
- if ($dtstart instanceof Property\ICalendar\Date) {
- // One day event
- if ($diff->days === 1) {
- return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- }
+ /**
+ * generates a when string based on if a event has an recurrence or not
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenString(EventReader $er): string {
+ return match ($er->recurs()) {
+ true => $this->generateWhenStringRecurring($er),
+ false => $this->generateWhenStringSingular($er)
+ };
+ }
- // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
- // the email should show 2020-01-01 to 2020-01-04.
- $dtendDt->modify('-1 day');
+ /**
+ * generates a when string for a non recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringSingular(EventReader $er): string {
+ // initialize
+ $startTime = null;
+ $endTime = null;
+ // calculate time difference from now to start of event
+ $occurring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+ // extract start date
+ $startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']);
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // In a minute/hour/day/week/month/year on July 1, 2024 for the entire day
+ // In a minute/hour/day/week/month/year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 for the entire day
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
+ return match ([$occurring['scale'], $endTime !== null]) {
+ ['past', false] => $this->l10n->t(
+ 'In the past on %1$s for the entire day',
+ [$startDate]
+ ),
+ ['minute', false] => $this->l10n->n(
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['hour', false] => $this->l10n->n(
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['day', false] => $this->l10n->n(
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['week', false] => $this->l10n->n(
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['month', false] => $this->l10n->n(
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['year', false] => $this->l10n->n(
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['past', true] => $this->l10n->t(
+ 'In the past on %1$s between %2$s - %3$s',
+ [$startDate, $startTime, $endTime]
+ ),
+ ['minute', true] => $this->l10n->n(
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['hour', true] => $this->l10n->n(
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['day', true] => $this->l10n->n(
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['week', true] => $this->l10n->n(
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['month', true] => $this->l10n->n(
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['year', true] => $this->l10n->n(
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ default => $this->l10n->t('Could not generate when statement')
+ };
+ }
- //event that spans over multiple days
- $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
+ /**
+ * generates a when string based on recurrence precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurring(EventReader $er): string {
+ return match ($er->recurringPrecision()) {
+ 'daily' => $this->generateWhenStringRecurringDaily($er),
+ 'weekly' => $this->generateWhenStringRecurringWeekly($er),
+ 'monthly' => $this->generateWhenStringRecurringMonthly($er),
+ 'yearly' => $this->generateWhenStringRecurringYearly($er),
+ 'fixed' => $this->generateWhenStringRecurringFixed($er),
+ };
+ }
- return $localeStart . ' - ' . $localeEnd;
+ /**
+ * generates a when string for a daily precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringDaily(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // Every Day for the entire day
+ // Every Day for the entire day until July 13, 2024
+ // Every Day between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ // Every 3 Days for the entire day
+ // Every 3 Days for the entire day until July 13, 2024
+ // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Day for the entire day'),
+ [false, false, true] => $this->l10n->t('Every Day for the entire day until %1$s', [$conclusion]),
+ [false, true, false] => $this->l10n->t('Every Day between %1$s - %2$s', [$startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Day between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Days for the entire day', [$interval]),
+ [true, false, true] => $this->l10n->t('Every %1$d Days for the entire day until %2$s', [$interval, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Days between %2$s - %3$s', [$interval, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Days between %2$s - %3$s until %4$s', [$interval, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- /** @var Property\ICalendar\DateTime $dtstart */
- /** @var Property\ICalendar\DateTime $dtend */
- $isFloating = $dtstart->isFloating();
- $startTimezone = $endTimezone = null;
- if (!$isFloating) {
- $prop = $dtstart->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $startTimezone = $prop->getValue();
- }
+ }
- $prop = $dtend->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $endTimezone = $prop->getValue();
- }
+ /**
+ * generates a when string for a weekly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringWeekly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // days of the week
+ $days = implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // Every Week on Monday, Wednesday, Friday for the entire day
+ // Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024
+ // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ // Every 2 Weeks on Monday, Wednesday, Friday for the entire day
+ // Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024
+ // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Week on %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Week on %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- $localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
- $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
-
- // always show full date with timezone if timezones are different
- if ($startTimezone !== $endTimezone) {
- $localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
+ }
- return $localeStart . ' (' . $startTimezone . ') - ' .
- $localeEnd . ' (' . $endTimezone . ')';
+ /**
+ * generates a when string for a monthly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringMonthly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
+ . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ } else {
+ $days = implode(', ', $er->recurringDaysOfMonth());
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order, output varies depending on if the event is absolute or releative:
+ // Absolute: Every Month on the 1, 8 for the entire day
+ // Relative: Every Month on the First Sunday, Saturday for the entire day
+ // Absolute: Every Month on the 1, 8 for the entire day until December 31, 2024
+ // Relative: Every Month on the First Sunday, Saturday for the entire day until December 31, 2024
+ // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Absolute: Every 2 Months on the 1, 8 for the entire day
+ // Relative: Every 2 Months on the First Sunday, Saturday for the entire day
+ // Absolute: Every 2 Months on the 1, 8 for the entire day until December 31, 2024
+ // Relative: Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024
+ // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Month on the %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Month on the %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
- // show only end time if date is the same
- if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
- $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
+ /**
+ * generates a when string for a yearly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringYearly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // months of year
+ $months = implode(', ', array_map(function ($value) { return $this->localizeMonthName($value); }, $er->recurringMonthsOfYearNamed()));
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
+ . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
} else {
- $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
- $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
+ $days = $er->startDateTime()->format('jS');
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order, output varies depending on if the event is absolute or releative:
+ // Absolute: Every Year in July on the 1st for the entire day
+ // Relative: Every Year in July on the First Sunday, Saturday for the entire day
+ // Absolute: Every Year in July on the 1st for the entire day until July 31, 2026
+ // Relative: Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026
+ // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Absolute: Every 2 Years in July on the 1st for the entire day
+ // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day
+ // Absolute: Every 2 Years in July on the 1st for the entire day until July 31, 2026
+ // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026
+ // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day', [$months, $days]),
+ [false, false, true] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day until %3$s', [$months, $days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s', [$months, $days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', [$months, $days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day', [$interval, $months, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [$interval, $months, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [$interval, $months, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [$interval, $months, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
+
+ /**
+ * generates a when string for a fixed precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringFixed(EventReader $er): string {
+ // initialize
+ $startTime = null;
+ $conclusion = null;
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // On specific dates for the entire day until July 13, 2024
+ // On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ($startTime !== null) {
+ false => $this->l10n->t('On specific dates for the entire day until %1$s', [$conclusion]),
+ true => $this->l10n->t('On specific dates between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ };
+ }
+
+ /**
+ * generates a occurring next string for a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateOccurringString(EventReader $er): string {
+
+ // initialize
+ $occurrence = null;
+ $occurrence2 = null;
+ $occurrence3 = null;
+ // reset to initial occurrence
+ $er->recurrenceRewind();
+ // forward to current date
+ $er->recurrenceAdvanceTo($this->timeFactory->getDateTime());
+ // calculate time difference from now to start of next event occurrence and minimize it
+ $occurrenceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+ // store next occurrence value
+ $occurrence = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ // forward one occurrence
+ $er->recurrenceAdvance();
+ // evaluate if occurrence is valid
+ if ($er->recurrenceDate() !== null) {
+ // store following occurrence value
+ $occurrence2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ // forward one occurrence
+ $er->recurrenceAdvance();
+ // evaluate if occurrence is valid
+ if ($er->recurrenceDate()) {
+ // store following occurrence value
+ $occurrence3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ }
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // In a minute/hour/day/week/month/year on July 1, 2024
+ // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024
+ // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024 and July 5, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024 and July 5, 2024
+ return match ([$occurrenceIn['scale'], $occurrence2 !== null, $occurrence3 !== null]) {
+ ['past', false, false] => $this->l10n->t(
+ 'In the past on %1$s',
+ [$occurrence]
+ ),
+ ['minute', false, false] => $this->l10n->n(
+ 'In a minute on %1$s',
+ 'In %n minutes on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['hour', false, false] => $this->l10n->n(
+ 'In a hour on %1$s',
+ 'In %n hours on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['day', false, false] => $this->l10n->n(
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['week', false, false] => $this->l10n->n(
+ 'In a week on %1$s',
+ 'In %n weeks on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['month', false, false] => $this->l10n->n(
+ 'In a month on %1$s',
+ 'In %n months on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['year', false, false] => $this->l10n->n(
+ 'In a year on %1$s',
+ 'In %n years on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['past', true, false] => $this->l10n->t(
+ 'In the past on %1$s then on %2$s',
+ [$occurrence, $occurrence2]
+ ),
+ ['minute', true, false] => $this->l10n->n(
+ 'In a minute on %1$s then on %2$s',
+ 'In %n minutes on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['hour', true, false] => $this->l10n->n(
+ 'In a hour on %1$s then on %2$s',
+ 'In %n hours on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['day', true, false] => $this->l10n->n(
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['week', true, false] => $this->l10n->n(
+ 'In a week on %1$s then on %2$s',
+ 'In %n weeks on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['month', true, false] => $this->l10n->n(
+ 'In a month on %1$s then on %2$s',
+ 'In %n months on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['year', true, false] => $this->l10n->n(
+ 'In a year on %1$s then on %2$s',
+ 'In %n years on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['past', true, true] => $this->l10n->t(
+ 'In the past on %1$s then on %2$s and %3$s',
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['minute', true, true] => $this->l10n->n(
+ 'In a minute on %1$s then on %2$s and %3$s',
+ 'In %n minutes on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['hour', true, true] => $this->l10n->n(
+ 'In a hour on %1$s then on %2$s and %3$s',
+ 'In %n hours on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['day', true, true] => $this->l10n->n(
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['week', true, true] => $this->l10n->n(
+ 'In a week on %1$s then on %2$s and %3$s',
+ 'In %n weeks on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['month', true, true] => $this->l10n->n(
+ 'In a month on %1$s then on %2$s and %3$s',
+ 'In %n months on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['year', true, true] => $this->l10n->n(
+ 'In a year on %1$s then on %2$s and %3$s',
+ 'In %n years on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ default => $this->l10n->t('Could not generate next recurrence statement')
+ };
- return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
}
/**
@@ -278,12 +789,13 @@ class IMipService {
* @return array
*/
public function buildCancelledBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
$defaultVal = '';
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
- $newMeetingWhen = $this->generateWhenString($vEvent);
+ $newMeetingWhen = $this->generateWhenString($eventReaderCurrent);
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
- ;
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
@@ -337,7 +849,7 @@ class IMipService {
return $dtEnd->getDateTime()->getTimeStamp();
}
- if(isset($component->DURATION)) {
+ if (isset($component->DURATION)) {
/** @var \DateTime $endDate */
$endDate = clone $dtStart->getDateTime();
// $component->DTEND->getDateTime() returns DateTimeImmutable
@@ -345,7 +857,7 @@ class IMipService {
return $endDate->getTimestamp();
}
- if(!$dtStart->hasTime()) {
+ if (!$dtStart->hasTime()) {
/** @var \DateTime $endDate */
// $component->DTSTART->getDateTime() returns DateTimeImmutable
$endDate = clone $dtStart->getDateTime();
@@ -361,7 +873,7 @@ class IMipService {
* @param Property|null $attendee
*/
public function setL10n(?Property $attendee = null) {
- if($attendee === null) {
+ if ($attendee === null) {
return;
}
@@ -377,7 +889,7 @@ class IMipService {
* @return bool
*/
public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
- if($attendee === null) {
+ if ($attendee === null) {
return false;
}
@@ -485,10 +997,10 @@ class IMipService {
htmlspecialchars($organizer->getNormalizedValue()),
htmlspecialchars($organizerName ?: $organizerEmail));
$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
- if(isset($organizer['PARTSTAT'])) {
+ if (isset($organizer['PARTSTAT'])) {
/** @var Parameter $partstat */
$partstat = $organizer['PARTSTAT'];
- if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
$organizerHTML .= ' ✔︎';
$organizerText .= ' ✔︎';
}
@@ -539,7 +1051,7 @@ class IMipService {
$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
if ($data['meeting_when'] !== '') {
- $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'),
+ $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('When:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_location'] !== '') {
@@ -550,6 +1062,10 @@ class IMipService {
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
}
+ if (isset($data['meeting_occurring'])) {
+ $template->addBodyListItem($data['meeting_occurring_html'] ?? $data['meeting_occurring'], $this->l10n->t('Occurring:'),
+ $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_occurring'], '', IMipPlugin::IMIP_INDENT);
+ }
$this->addAttendees($template, $vevent);
@@ -569,8 +1085,11 @@ class IMipService {
$vevent = $iTipMessage->message->VEVENT;
$attendees = $vevent->select('ATTENDEE');
foreach ($attendees as $attendee) {
- /** @var Property $attendee */
- if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
+ if ($iTipMessage->method === 'REPLY' && strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) {
+ /** @var Property $attendee */
+ return $attendee;
+ } elseif (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
+ /** @var Property $attendee */
return $attendee;
}
}
@@ -589,9 +1108,9 @@ class IMipService {
$attendee = $iTipMessage->recipient;
$organizer = $iTipMessage->sender;
$sequence = $iTipMessage->sequence;
- $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
- $vevent->{'RECURRENCE-ID'}->serialize() : null;
- $uid = $vevent->{'UID'};
+ $recurrenceId = isset($vevent->{'RECURRENCE-ID'})
+ ? $vevent->{'RECURRENCE-ID'}->serialize() : null;
+ $uid = $vevent->{'UID'}?->getValue();
$query = $this->db->getQueryBuilder();
$query->insert('calendar_invitations')
@@ -604,37 +1123,12 @@ class IMipService {
'expiration' => $query->createNamedParameter($lastOccurrence),
'uid' => $query->createNamedParameter($uid)
])
- ->execute();
+ ->executeStatement();
return $token;
}
/**
- * Create a valid VCalendar object out of the details of
- * a VEvent and its associated iTip Message
- *
- * We do this to filter out all unchanged VEvents
- * This is especially important in iTip Messages with recurrences
- * and recurrence exceptions
- *
- * @param Message $iTipMessage
- * @param VEvent $vEvent
- * @return VCalendar
- */
- public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
- $vCalendar = new VCalendar();
- $vCalendar->add('METHOD', $iTipMessage->method);
- foreach ($iTipMessage->message->getComponents() as $component) {
- if ($component instanceof VEvent) {
- continue;
- }
- $vCalendar->add(clone $component);
- }
- $vCalendar->add($vEvent);
- return $vCalendar;
- }
-
- /**
* @param IEMailTemplate $template
* @param $token
*/
@@ -678,14 +1172,123 @@ class IMipService {
public function isRoomOrResource(Property $attendee): bool {
$cuType = $attendee->offsetGet('CUTYPE');
- if(!$cuType instanceof Parameter) {
+ if (!$cuType instanceof Parameter) {
return false;
}
$type = $cuType->getValue() ?? 'INDIVIDUAL';
- if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM', 'UNKNOWN'], true)) {
+ if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM'], true)) {
// Don't send emails to things
return true;
}
return false;
}
+
+ public function isCircle(Property $attendee): bool {
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if (!$cuType instanceof Parameter) {
+ return false;
+ }
+
+ $uri = $attendee->getValue();
+ if (!$uri) {
+ return false;
+ }
+
+ $cuTypeValue = $cuType->getValue();
+ return $cuTypeValue === 'GROUP' && str_starts_with($uri, 'mailto:circle+');
+ }
+
+ public function minimizeInterval(\DateInterval $dateInterval): array {
+ // evaluate if time interval is in the past
+ if ($dateInterval->invert == 1) {
+ return ['interval' => 1, 'scale' => 'past'];
+ }
+ // evaluate interval parts and return smallest time period
+ if ($dateInterval->y > 0) {
+ $interval = $dateInterval->y;
+ $scale = 'year';
+ } elseif ($dateInterval->m > 0) {
+ $interval = $dateInterval->m;
+ $scale = 'month';
+ } elseif ($dateInterval->d >= 7) {
+ $interval = (int)($dateInterval->d / 7);
+ $scale = 'week';
+ } elseif ($dateInterval->d > 0) {
+ $interval = $dateInterval->d;
+ $scale = 'day';
+ } elseif ($dateInterval->h > 0) {
+ $interval = $dateInterval->h;
+ $scale = 'hour';
+ } else {
+ $interval = $dateInterval->i;
+ $scale = 'minute';
+ }
+
+ return ['interval' => $interval, 'scale' => $scale];
+ }
+
+ /**
+ * Localizes week day names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeDayName(string $value): string {
+ return match ($value) {
+ 'Monday' => $this->l10n->t('Monday'),
+ 'Tuesday' => $this->l10n->t('Tuesday'),
+ 'Wednesday' => $this->l10n->t('Wednesday'),
+ 'Thursday' => $this->l10n->t('Thursday'),
+ 'Friday' => $this->l10n->t('Friday'),
+ 'Saturday' => $this->l10n->t('Saturday'),
+ 'Sunday' => $this->l10n->t('Sunday'),
+ };
+ }
+
+ /**
+ * Localizes month names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeMonthName(string $value): string {
+ return match ($value) {
+ 'January' => $this->l10n->t('January'),
+ 'February' => $this->l10n->t('February'),
+ 'March' => $this->l10n->t('March'),
+ 'April' => $this->l10n->t('April'),
+ 'May' => $this->l10n->t('May'),
+ 'June' => $this->l10n->t('June'),
+ 'July' => $this->l10n->t('July'),
+ 'August' => $this->l10n->t('August'),
+ 'September' => $this->l10n->t('September'),
+ 'October' => $this->l10n->t('October'),
+ 'November' => $this->l10n->t('November'),
+ 'December' => $this->l10n->t('December'),
+ };
+ }
+
+ /**
+ * Localizes relative position names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeRelativePositionName(string $value): string {
+ return match ($value) {
+ 'First' => $this->l10n->t('First'),
+ 'Second' => $this->l10n->t('Second'),
+ 'Third' => $this->l10n->t('Third'),
+ 'Fourth' => $this->l10n->t('Fourth'),
+ 'Fifth' => $this->l10n->t('Fifth'),
+ 'Last' => $this->l10n->t('Last'),
+ 'Second Last' => $this->l10n->t('Second Last'),
+ 'Third Last' => $this->l10n->t('Third Last'),
+ 'Fourth Last' => $this->l10n->t('Fourth Last'),
+ 'Fifth Last' => $this->l10n->t('Fifth Last'),
+ };
+ }
}
diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php
index a1297fd2cf1..a001df8b2a8 100644
--- a/apps/dav/lib/CalDAV/Schedule/Plugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php
@@ -1,31 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
- * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Schedule;
@@ -33,11 +10,15 @@ use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\TipBroker;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Schedule\ISchedulingObject;
+use Sabre\DAV\Exception as DavException;
use Sabre\DAV\INode;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropFind;
@@ -61,11 +42,6 @@ use function Sabre\Uri\split;
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
- /**
- * @var IConfig
- */
- private $config;
-
/** @var ITip\Message[] */
private $schedulingResponses = [];
@@ -74,14 +50,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
- private LoggerInterface $logger;
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config, LoggerInterface $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
}
/**
@@ -105,6 +82,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
}
/**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): TipBroker {
+ return new TipBroker();
+ }
+
+ /**
* Allow manual setting of the object change URL
* to support public write
*
@@ -155,6 +139,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$result = [];
}
+ // iterate through items and html decode values
+ foreach ($result as $key => $value) {
+ $result[$key] = urldecode($value);
+ }
+
return $result;
}
@@ -173,7 +162,47 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
}
try {
- parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
+
+ // Do not generate iTip and iMip messages if scheduling is disabled for this message
+ if ($request->getHeader('x-nc-scheduling') === 'false') {
+ return;
+ }
+
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ /** @var Calendar $calendarNode */
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+ // extract addresses for owner
+ $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
+ // determine if request is from a sharee
+ if ($calendarNode->isShared()) {
+ // extract addresses for sharee and add to address collection
+ $addresses = array_merge(
+ $addresses,
+ $this->getAddressesForPrincipal($calendarNode->getPrincipalURI())
+ );
+ }
+ // determine if we are updating a calendar event
+ if (!$isNew) {
+ // retrieve current calendar event node
+ /** @var CalendarObject $currentNode */
+ $currentNode = $this->server->tree->getNodeForPath($request->getPath());
+ // convert calendar event string data to VCalendar object
+ /** @var \Sabre\VObject\Component\VCalendar $currentObject */
+ $currentObject = Reader::read($currentNode->get());
+ } else {
+ $currentObject = null;
+ }
+ // process request
+ $this->processICalendarChange($currentObject, $vCal, $addresses, [], $modified);
+
+ if ($currentObject) {
+ // Destroy circular references so PHP will GC the object.
+ $currentObject->destroy();
+ }
+
} catch (SameOrganizerForAllComponentsException $e) {
$this->handleSameOrganizerException($e, $vCal, $calendarPath);
}
@@ -232,7 +261,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
$calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
- $this->logger->debug('Calendar user type is room or resource, not processing further');
+ $this->logger->debug('Calendar user type is neither room nor resource, not processing further');
return;
}
@@ -343,8 +372,8 @@ EOF;
return null;
}
- $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
- str_starts_with($principalUrl, 'principals/calendar-rooms');
+ $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources')
+ || str_starts_with($principalUrl, 'principals/calendar-rooms');
if (str_starts_with($principalUrl, 'principals/users')) {
[, $userId] = split($principalUrl);
@@ -379,11 +408,20 @@ EOF;
* - isn't a calendar subscription
* - user can write to it (no virtual/3rd-party calendars)
* - calendar isn't a share
+ * - calendar supports VEVENTs
*/
foreach ($calendarHome->getChildren() as $node) {
- if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) {
- $userCalendars[] = $node;
+ if (!($node instanceof Calendar)) {
+ continue;
}
+
+ try {
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ } catch (DavException $e) {
+ continue;
+ }
+
+ $userCalendars[] = $node;
}
if (count($userCalendars) > 0) {
@@ -392,12 +430,20 @@ EOF;
} else {
// Otherwise if we have really nothing, create a new calendar
if ($currentCalendarDeleted) {
- // If the calendar exists but is deleted, we need to purge it first
- // This may cause some issues in a non synchronous database setup
+ // If the calendar exists but is in the trash bin, we try to rename its uri
+ // so that we can create the new one and still restore the previous one
+ // otherwise we just purge the calendar by removing it before recreating it
$calendar = $this->getCalendar($calendarHome, $uri);
if ($calendar instanceof Calendar) {
- $calendar->disableTrashbin();
- $calendar->delete();
+ $backend = $calendarHome->getCalDAVBackend();
+ if ($backend instanceof CalDavBackend) {
+ // If the CalDAV backend supports moving calendars
+ $this->moveCalendar($backend, $principalUrl, $uri, $uri . '-back-' . time());
+ } else {
+ // Otherwise just purge the calendar
+ $calendar->disableTrashbin();
+ $calendar->delete();
+ }
}
}
$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
@@ -532,7 +578,9 @@ EOF;
$calendarTimeZone = new DateTimeZone('UTC');
$homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
+ /** @var Calendar $node */
foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
+
if (!$node instanceof ICalendar) {
continue;
}
@@ -664,6 +712,10 @@ EOF;
]);
}
+ private function moveCalendar(CalDavBackend $calDavBackend, string $principalUri, string $oldUri, string $newUri): void {
+ $calDavBackend->moveCalendar($oldUri, $principalUri, $principalUri, $newUri);
+ }
+
/**
* Try to handle the given exception gracefully or throw it if necessary.
*
diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
index d08a5749ab2..27e39a76305 100644
--- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php
+++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
@@ -1,33 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
@@ -81,8 +62,8 @@ class SearchPlugin extends ServerPlugin {
$server->on('report', [$this, 'report']);
- $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] =
- CalendarSearchReport::class;
+ $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search']
+ = CalendarSearchReport::class;
}
/**
@@ -129,7 +110,7 @@ class SearchPlugin extends ServerPlugin {
* This report is used by clients to request calendar objects based on
* complex conditions.
*
- * @param Xml\Request\CalendarSearchReport $report
+ * @param CalendarSearchReport $report
* @return void
*/
private function calendarSearch($report) {
@@ -154,7 +135,7 @@ class SearchPlugin extends ServerPlugin {
$prefer = $this->server->getHTTPPrefer();
- $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS);
$this->server->httpResponse->setHeader('Content-Type',
'application/xml; charset=utf-8');
$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
index d5b7c834e36..21a4fff1caf 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
index 2c435ba3650..a98b325397b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
index a6f41d09161..ef438aa0258 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
index c25450a0c94..0c31f32348a 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
index 990b0ebf730..251120e35cc 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
index 06fe39a463b..6d6bf958496 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
index 98efe36ee43..6ece88fa87b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Request;
diff --git a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
index 8ee4c9cb4d3..311157994e2 100644
--- a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
+++ b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Security;
@@ -41,24 +25,16 @@ use function explode;
class RateLimitingPlugin extends ServerPlugin {
private Limiter $limiter;
- private IUserManager $userManager;
- private CalDavBackend $calDavBackend;
- private IAppConfig $config;
- private LoggerInterface $logger;
- private ?string $userId;
- public function __construct(Limiter $limiter,
- IUserManager $userManager,
- CalDavBackend $calDavBackend,
- LoggerInterface $logger,
- IAppConfig $config,
- ?string $userId) {
+ public function __construct(
+ Limiter $limiter,
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private IAppConfig $config,
+ private ?string $userId,
+ ) {
$this->limiter = $limiter;
- $this->userManager = $userManager;
- $this->calDavBackend = $calDavBackend;
- $this->config = $config;
- $this->logger = $logger;
- $this->userId = $userId;
}
public function initialize(DAV\Server $server): void {
diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php
index 7a87f0353e7..fc5d65b5994 100644
--- a/apps/dav/lib/CalDAV/Sharing/Backend.php
+++ b/apps/dav/lib/CalDAV/Sharing/Backend.php
@@ -2,22 +2,8 @@
declare(strict_types=1);
/**
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Sharing;
@@ -31,7 +17,8 @@ use Psr\Log\LoggerInterface;
class Backend extends SharingBackend {
- public function __construct(private IUserManager $userManager,
+ public function __construct(
+ private IUserManager $userManager,
private IGroupManager $groupManager,
private Principal $principalBackend,
private ICacheFactory $cacheFactory,
diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php
index cdf8c892ab5..4f0554f09bd 100644
--- a/apps/dav/lib/CalDAV/Sharing/Service.php
+++ b/apps/dav/lib/CalDAV/Sharing/Service.php
@@ -2,22 +2,8 @@
declare(strict_types=1);
/**
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Sharing;
@@ -27,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService;
class Service extends SharingService {
protected string $resourceType = 'calendar';
- public function __construct(protected SharingMapper $mapper) {
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
parent::__construct($mapper);
}
}
diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php
index 29129a3b073..9ee0e9bf356 100644
--- a/apps/dav/lib/CalDAV/Status/StatusService.php
+++ b/apps/dav/lib/CalDAV/Status/StatusService.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2023 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Status;
@@ -32,6 +15,7 @@ use OCA\UserStatus\Service\StatusService as UserStatusService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\IManager;
+use OCP\DB\Exception;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUser as User;
@@ -43,36 +27,49 @@ use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
class StatusService {
private ICache $cache;
- public function __construct(private ITimeFactory $timeFactory,
+ public function __construct(
+ private ITimeFactory $timeFactory,
private IManager $calendarManager,
private IUserManager $userManager,
private UserStatusService $userStatusService,
private IAvailabilityCoordinator $availabilityCoordinator,
private ICacheFactory $cacheFactory,
- private LoggerInterface $logger) {
+ private LoggerInterface $logger,
+ ) {
$this->cache = $cacheFactory->createLocal('CalendarStatusService');
}
public function processCalendarStatus(string $userId): void {
$user = $this->userManager->get($userId);
- if($user === null) {
+ if ($user === null) {
return;
}
$availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
- if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
+ if ($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
$this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]);
return;
}
$calendarEvents = $this->cache->get($userId);
- if($calendarEvents === null) {
+ if ($calendarEvents === null) {
$calendarEvents = $this->getCalendarEvents($user);
$this->cache->set($userId, $calendarEvents, 300);
}
- if(empty($calendarEvents)) {
- $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ if (empty($calendarEvents)) {
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
$this->logger->debug('No calendar events found for status check', ['user' => $userId]);
return;
}
@@ -86,9 +83,9 @@ class StatusService {
$currentStatus = null;
}
- if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL
- || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND
- || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) {
+ if (($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL)
+ || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND)
+ || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE)) {
// We don't overwrite the call status, DND status or Invisible status
$this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]);
return;
@@ -106,7 +103,7 @@ class StatusService {
if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
/** @var DateTimeImmutable $dateTime */
$dateTime = $component['DTSTART'][0];
- if($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
+ if ($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
return false;
}
}
@@ -117,14 +114,25 @@ class StatusService {
return true;
});
- if(empty($applicableEvents)) {
- $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ if (empty($applicableEvents)) {
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
$this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
return;
}
// Only update the status if it's neccesary otherwise we mess up the timestamp
- if($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
+ if ($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
// One event that fulfills all status conditions is enough
// 1. Not an OOO event
// 2. Current user status (that is not a calendar status) was not set after the start of this event
@@ -133,7 +141,7 @@ class StatusService {
$this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
$this->userStatusService->setUserStatus(
$userId,
- IUserStatus::AWAY,
+ IUserStatus::BUSY,
IUserStatus::MESSAGE_CALENDAR_BUSY,
true
);
@@ -142,7 +150,7 @@ class StatusService {
private function getCalendarEvents(User $user): array {
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
- if(empty($calendars)) {
+ if (empty($calendars)) {
return [];
}
@@ -166,7 +174,7 @@ class StatusService {
$dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes'));
// Only query the calendars when there's any to search
- if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
+ if ($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
// Query the next hour
$query->setTimerangeStart($dtStart);
$query->setTimerangeEnd($dtEnd);
diff --git a/apps/dav/lib/CalDAV/TimeZoneFactory.php b/apps/dav/lib/CalDAV/TimeZoneFactory.php
new file mode 100644
index 00000000000..36a2c97be82
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TimeZoneFactory.php
@@ -0,0 +1,213 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTimeZone;
+
+/**
+ * Class to generate DateTimeZone object with automated Microsoft and IANA handling
+ *
+ * @since 31.0.0
+ */
+class TimeZoneFactory {
+
+ /**
+ * conversion table of Microsoft time zones to IANA time zones
+ *
+ * @var array<string,string> MS2IANA
+ */
+ private const MS2IANA = [
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ 'Aus Central W. Standard Time' => 'Australia/Eucla',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ 'Aleutian Standard Time' => 'America/Adak',
+ 'Altai Standard Time' => 'Asia/Barnaul',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ 'Astrakhan Standard Time' => 'Europe/Astrakhan',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ 'Belarus Standard Time' => 'Europe/Minsk',
+ 'Bougainville Standard Time' => 'Pacific/Bougainville',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ 'Canada Central Standard Time' => 'America/Regina',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ 'Central America Standard Time' => 'America/Guatemala',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ 'Chatham Islands Standard Time' => 'Pacific/Chatham',
+ 'China Standard Time' => 'Asia/Shanghai',
+ 'Coordinated Universal Time' => 'UTC',
+ 'Cuba Standard Time' => 'America/Havana',
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ 'Easter Island Standard Time' => 'Pacific/Easter',
+ 'Eastern Standard Time' => 'America/Toronto',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ 'GMT Standard Time' => 'Europe/London',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ 'Greenland Standard Time' => 'America/Godthab',
+ 'Greenland (Danmarkshavn)' => 'America/Godthab',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ 'Haiti Standard Time' => 'America/Port-au-Prince',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ 'India Standard Time' => 'Asia/Kolkata',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ 'Kamchatka Standard Time' => 'Asia/Kamchatka',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
+ 'Lord Howe Standard Time' => 'Australia/Lord_Howe',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ 'Magallanes Standard Time' => 'America/Punta_Arenas',
+ 'Malaysia Standard Time' => 'Asia/Kuala_Lumpur',
+ 'Marquesas Standard Time' => 'Pacific/Marquesas',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ 'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ 'Nepal Standard Time' => 'Asia/Kathmandu',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ 'Norfolk Standard Time' => 'Pacific/Norfolk',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
+ 'Omsk Standard Time' => 'Asia/Omsk',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
+ 'Romance Standard Time' => 'Europe/Paris',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+ 'Russia Time Zone 3' => 'Europe/Samara',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ 'Saint Pierre Standard Time' => 'America/Miquelon',
+ 'Sakhalin Standard Time' => 'Asia/Sakhalin',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ 'Sao Tome Standard Time' => 'Africa/Sao_Tome',
+ 'Saratov Standard Time' => 'Europe/Saratov',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ 'South Sudan Standard Time' => 'Africa/Juba',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ 'Sudan Standard Time' => 'Africa/Khartoum',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ 'Tocantins Standard Time' => 'America/Araguaina',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ 'Tomsk Standard Time' => 'Asia/Tomsk',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ 'Transbaikal Standard Time' => 'Asia/Chita',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ 'UTC' => 'Etc/GMT',
+ 'UTC+13' => 'Etc/GMT-13',
+ 'UTC+12' => 'Etc/GMT-12',
+ 'UTC-02' => 'Etc/GMT+2',
+ 'UTC-09' => 'Etc/GMT+9',
+ 'UTC-11' => 'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ 'Volgograd Standard Time' => 'Europe/Volgograd',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ 'W. Mongolia Standard Time' => 'Asia/Hovd',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ 'West Bank Standard Time' => 'Asia/Hebron',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ 'West Samoa Standard Time' => 'Pacific/Apia',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ 'Yukon Standard Time' => 'America/Whitehorse',
+ 'Yekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ ];
+
+ /**
+ * Determines if given time zone name is a Microsoft time zone
+ *
+ * @since 31.0.0
+ *
+ * @param string $name time zone name
+ *
+ * @return bool
+ */
+ public static function isMS(string $name): bool {
+ return isset(self::MS2IANA[$name]);
+ }
+
+ /**
+ * Converts Microsoft time zone name to IANA time zone name
+ *
+ * @since 31.0.0
+ *
+ * @param string $name microsoft time zone
+ *
+ * @return string|null valid IANA time zone name on success, or null on failure
+ */
+ public static function toIANA(string $name): ?string {
+ return isset(self::MS2IANA[$name]) ? self::MS2IANA[$name] : null;
+ }
+
+ /**
+ * Generates DateTimeZone object for given time zone name
+ *
+ * @since 31.0.0
+ *
+ * @param string $name time zone name
+ *
+ * @return DateTimeZone|null
+ */
+ public function fromName(string $name): ?DateTimeZone {
+ // if zone name is MS convert to IANA, otherwise just assume the zone is IANA
+ $zone = @timezone_open(self::toIANA($name) ?? $name);
+ return ($zone instanceof DateTimeZone) ? $zone : null;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/TimezoneService.php b/apps/dav/lib/CalDAV/TimezoneService.php
index 1b8855a7215..a7709bde0f9 100644
--- a/apps/dav/lib/CalDAV/TimezoneService.php
+++ b/apps/dav/lib/CalDAV/TimezoneService.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -36,9 +20,11 @@ use function array_reduce;
class TimezoneService {
- public function __construct(private IConfig $config,
+ public function __construct(
+ private IConfig $config,
private PropertyMapper $propertyMapper,
- private IManager $calendarManager) {
+ private IManager $calendarManager,
+ ) {
}
public function getUserTimezone(string $userId): ?string {
diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php
new file mode 100644
index 00000000000..16e68fde1f0
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TipBroker.php
@@ -0,0 +1,187 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip\Broker;
+use Sabre\VObject\ITip\Message;
+
+class TipBroker extends Broker {
+
+ public $significantChangeProperties = [
+ 'DTSTART',
+ 'DTEND',
+ 'DURATION',
+ 'DUE',
+ 'RRULE',
+ 'RDATE',
+ 'EXDATE',
+ 'STATUS',
+ 'SUMMARY',
+ 'DESCRIPTION',
+ 'LOCATION',
+
+ ];
+
+ /**
+ * This method is used in cases where an event got updated, and we
+ * potentially need to send emails to attendees to let them know of updates
+ * in the events.
+ *
+ * We will detect which attendees got added, which got removed and create
+ * specific messages for these situations.
+ *
+ * @return array
+ */
+ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
+ // Merging attendee lists.
+ $attendees = [];
+ foreach ($oldEventInfo['attendees'] as $attendee) {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => $attendee['instances'],
+ 'newInstances' => [],
+ 'name' => $attendee['name'],
+ 'forceSend' => null,
+ ];
+ }
+ foreach ($eventInfo['attendees'] as $attendee) {
+ if (isset($attendees[$attendee['href']])) {
+ $attendees[$attendee['href']]['name'] = $attendee['name'];
+ $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
+ $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
+ } else {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => [],
+ 'newInstances' => $attendee['instances'],
+ 'name' => $attendee['name'],
+ 'forceSend' => $attendee['forceSend'],
+ ];
+ }
+ }
+
+ $messages = [];
+
+ foreach ($attendees as $attendee) {
+ // An organizer can also be an attendee. We should not generate any
+ // messages for those.
+ if ($attendee['href'] === $eventInfo['organizer']) {
+ continue;
+ }
+
+ $message = new Message();
+ $message->uid = $eventInfo['uid'];
+ $message->component = 'VEVENT';
+ $message->sequence = $eventInfo['sequence'];
+ $message->sender = $eventInfo['organizer'];
+ $message->senderName = $eventInfo['organizerName'];
+ $message->recipient = $attendee['href'];
+ $message->recipientName = $attendee['name'];
+
+ // Creating the new iCalendar body.
+ $icalMsg = new VCalendar();
+
+ foreach ($calendar->select('VTIMEZONE') as $timezone) {
+ $icalMsg->add(clone $timezone);
+ }
+ // If there are no instances the attendee is a part of, it means
+ // the attendee was removed and we need to send them a CANCEL message.
+ // Also If the meeting STATUS property was changed to CANCELLED
+ // we need to send the attendee a CANCEL message.
+ if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') {
+
+ $message->method = $icalMsg->METHOD = 'CANCEL';
+ $message->significantChange = true;
+ // clone base event
+ $event = clone $eventInfo['instances']['master'];
+ // alter some properties
+ unset($event->ATTENDEE);
+ $event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]);
+ $event->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $event->SEQUENCE = $message->sequence;
+ $icalMsg->add($event);
+
+ } else {
+ // The attendee gets the updated event body
+ $message->method = $icalMsg->METHOD = 'REQUEST';
+
+ // We need to find out that this change is significant. If it's
+ // not, systems may opt to not send messages.
+ //
+ // We do this based on the 'significantChangeHash' which is
+ // some value that changes if there's a certain set of
+ // properties changed in the event, or simply if there's a
+ // difference in instances that the attendee is invited to.
+
+ $oldAttendeeInstances = array_keys($attendee['oldInstances']);
+ $newAttendeeInstances = array_keys($attendee['newInstances']);
+
+ $message->significantChange
+ = $attendee['forceSend'] === 'REQUEST'
+ || count($oldAttendeeInstances) !== count($newAttendeeInstances)
+ || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0
+ || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
+
+ foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
+ $currentEvent = clone $eventInfo['instances'][$instanceId];
+ if ($instanceId === 'master') {
+ // We need to find a list of events that the attendee
+ // is not a part of to add to the list of exceptions.
+ $exceptions = [];
+ foreach ($eventInfo['instances'] as $instanceId => $vevent) {
+ if (!isset($attendee['newInstances'][$instanceId])) {
+ $exceptions[] = $instanceId;
+ }
+ }
+
+ // If there were exceptions, we need to add it to an
+ // existing EXDATE property, if it exists.
+ if ($exceptions) {
+ if (isset($currentEvent->EXDATE)) {
+ $currentEvent->EXDATE->setParts(array_merge(
+ $currentEvent->EXDATE->getParts(),
+ $exceptions
+ ));
+ } else {
+ $currentEvent->EXDATE = $exceptions;
+ }
+ }
+
+ // Cleaning up any scheduling information that
+ // shouldn't be sent along.
+ unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
+ unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
+
+ foreach ($currentEvent->ATTENDEE as $attendee) {
+ unset($attendee['SCHEDULE-FORCE-SEND']);
+ unset($attendee['SCHEDULE-STATUS']);
+
+ // We're adding PARTSTAT=NEEDS-ACTION to ensure that
+ // iOS shows an "Inbox Item"
+ if (!isset($attendee['PARTSTAT'])) {
+ $attendee['PARTSTAT'] = 'NEEDS-ACTION';
+ }
+ }
+ }
+
+ $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $icalMsg->add($currentEvent);
+ }
+ }
+
+ $message->message = $icalMsg;
+ $messages[] = $message;
+ }
+
+ return $messages;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
index b1a3fea21b7..d8c429f2056 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -35,26 +18,13 @@ use Sabre\DAVACL\IACL;
class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
use ACLTrait;
- /** @var string */
- private $name;
-
- /** @var mixed[] */
- private $objectData;
-
- /** @var string */
- private $principalUri;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- public function __construct(string $name,
- array $objectData,
- string $principalUri,
- CalDavBackend $calDavBackend) {
- $this->name = $name;
- $this->objectData = $objectData;
- $this->calDavBackend = $calDavBackend;
- $this->principalUri = $principalUri;
+ public function __construct(
+ private string $name,
+ /** @var mixed[] */
+ private array $objectData,
+ private string $principalUri,
+ private CalDavBackend $calDavBackend,
+ ) {
}
public function delete() {
@@ -89,7 +59,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
public function getContentType() {
$mime = 'text/calendar; charset=utf-8';
if (isset($this->objectData['component']) && $this->objectData['component']) {
- $mime .= '; component='.$this->objectData['component'];
+ $mime .= '; component=' . $this->objectData['component'];
}
return $mime;
@@ -100,7 +70,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
}
public function getSize() {
- return (int) $this->objectData['size'];
+ return (int)$this->objectData['size'];
}
public function restore(): void {
@@ -108,7 +78,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
}
public function getDeletedAt(): ?int {
- return $this->objectData['deleted_at'] ? (int) $this->objectData['deleted_at'] : null;
+ return $this->objectData['deleted_at'] ? (int)$this->objectData['deleted_at'] : null;
}
public function getCalendarUri(): string {
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
index 5362b45ee7b..f75e19689f1 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -42,16 +25,11 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL
public const NAME = 'objects';
- /** @var CalDavBackend */
- protected $caldavBackend;
-
- /** @var mixed[] */
- private $principalInfo;
-
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ /** @var mixed[] */
+ private array $principalInfo,
+ ) {
}
/**
@@ -68,7 +46,7 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL
$data = $this->caldavBackend->getCalendarObjectById(
$this->principalInfo['uri'],
- (int) $matches[1],
+ (int)$matches[1],
);
// If the object hasn't been deleted yet then we don't want to find it here
diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
index 8a661732f19..6f58b1f3110 100644
--- a/apps/dav/lib/CalDAV/Trashbin/Plugin.php
+++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -49,16 +32,14 @@ class Plugin extends ServerPlugin {
/** @var bool */
private $disableTrashbin;
- /** @var RetentionService */
- private $retentionService;
-
/** @var Server */
private $server;
- public function __construct(IRequest $request,
- RetentionService $retentionService) {
+ public function __construct(
+ IRequest $request,
+ private RetentionService $retentionService,
+ ) {
$this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1';
- $this->retentionService = $retentionService;
}
public function initialize(Server $server): void {
diff --git a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
index 31331957c49..6641148de2b 100644
--- a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
+++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
index e9bf6da19e8..1c76bd2295d 100644
--- a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
+++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -43,16 +26,10 @@ class TrashbinHome implements IACL, ICollection, IProperties {
public const NAME = 'trashbin';
- /** @var CalDavBackend */
- private $caldavBackend;
-
- /** @var array */
- private $principalInfo;
-
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ private CalDavBackend $caldavBackend,
+ private array $principalInfo,
+ ) {
}
public function getOwner(): string {
diff --git a/apps/dav/lib/CalDAV/UpcomingEvent.php b/apps/dav/lib/CalDAV/UpcomingEvent.php
new file mode 100644
index 00000000000..e8b604f460a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/UpcomingEvent.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use JsonSerializable;
+use OCA\DAV\ResponseDefinitions;
+
+class UpcomingEvent implements JsonSerializable {
+ public function __construct(
+ private string $uri,
+ private ?int $recurrenceId,
+ private string $calendarUri,
+ private ?int $start,
+ private ?string $summary,
+ private ?string $location,
+ private ?string $calendarAppUrl,
+ ) {
+ }
+
+ public function getUri(): string {
+ return $this->uri;
+ }
+
+ public function getRecurrenceId(): ?int {
+ return $this->recurrenceId;
+ }
+
+ public function getCalendarUri(): string {
+ return $this->calendarUri;
+ }
+
+ public function getStart(): ?int {
+ return $this->start;
+ }
+
+ public function getSummary(): ?string {
+ return $this->summary;
+ }
+
+ public function getLocation(): ?string {
+ return $this->location;
+ }
+
+ public function getCalendarAppUrl(): ?string {
+ return $this->calendarAppUrl;
+ }
+
+ /**
+ * @see ResponseDefinitions
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'uri' => $this->uri,
+ 'recurrenceId' => $this->recurrenceId,
+ 'calendarUri' => $this->calendarUri,
+ 'start' => $this->start,
+ 'summary' => $this->summary,
+ 'location' => $this->location,
+ 'calendarAppUrl' => $this->calendarAppUrl,
+ ];
+ }
+}
diff --git a/apps/dav/lib/CalDAV/UpcomingEventsService.php b/apps/dav/lib/CalDAV/UpcomingEventsService.php
new file mode 100644
index 00000000000..1a8aed5bd71
--- /dev/null
+++ b/apps/dav/lib/CalDAV/UpcomingEventsService.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCP\App\IAppManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\IManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use function array_map;
+
+class UpcomingEventsService {
+ public function __construct(
+ private IManager $calendarManager,
+ private ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ private IAppManager $appManager,
+ private IURLGenerator $urlGenerator,
+ ) {
+ }
+
+ /**
+ * @return UpcomingEvent[]
+ */
+ public function getEvents(string $userId, ?string $location = null): array {
+ $searchQuery = $this->calendarManager->newQuery('principals/users/' . $userId);
+ if ($location !== null) {
+ $searchQuery->addSearchProperty('LOCATION');
+ $searchQuery->setSearchPattern($location);
+ }
+ $searchQuery->addType('VEVENT');
+ $searchQuery->setLimit(3);
+ $now = $this->timeFactory->now();
+ $searchQuery->setTimerangeStart($now->modify('-1 minute'));
+ $searchQuery->setTimerangeEnd($now->modify('+1 month'));
+
+ $events = $this->calendarManager->searchForPrincipal($searchQuery);
+ $calendarAppEnabled = $this->appManager->isEnabledForUser(
+ 'calendar',
+ $this->userManager->get($userId),
+ );
+
+ return array_filter(array_map(function (array $event) use ($userId, $calendarAppEnabled) {
+ $calendarAppUrl = null;
+
+ if ($calendarAppEnabled) {
+ $arguments = [
+ 'objectId' => base64_encode($this->urlGenerator->getWebroot() . '/remote.php/dav/calendars/' . $userId . '/' . $event['calendar-uri'] . '/' . $event['uri']),
+ ];
+
+ if (isset($event['RECURRENCE-ID'])) {
+ $arguments['recurrenceId'] = $event['RECURRENCE-ID'][0];
+ }
+ /**
+ * TODO: create a named, deep route in calendar (it's a code smell to just assume this route exists, find an abstraction)
+ * When changing, also adjust for:
+ * - spreed/lib/Service/CalendarIntegrationService.php#getDashboardEvents
+ * - spreed/lib/Service/CalendarIntegrationService.php#getMutualEvents
+ */
+ $calendarAppUrl = $this->urlGenerator->linkToRouteAbsolute('calendar.view.indexdirect.edit', $arguments);
+ }
+
+ if (isset($event['objects'][0]['STATUS']) && $event['objects'][0]['STATUS'][0] === 'CANCELLED') {
+ return false;
+ }
+
+ return new UpcomingEvent(
+ $event['uri'],
+ ($event['objects'][0]['RECURRENCE-ID'][0] ?? null)?->getTimeStamp(),
+ $event['calendar-uri'],
+ $event['objects'][0]['DTSTART'][0]?->getTimestamp(),
+ $event['objects'][0]['SUMMARY'][0] ?? null,
+ $event['objects'][0]['LOCATION'][0] ?? null,
+ $calendarAppUrl,
+ );
+ }, $events));
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php
new file mode 100644
index 00000000000..b647e63e67b
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Validation;
+
+use OCA\DAV\AppInfo\Application;
+use OCP\IAppConfig;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class CalDavValidatePlugin extends ServerPlugin {
+
+ public function __construct(
+ private IAppConfig $config,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $server->on('beforeMethod:PUT', [$this, 'beforePut']);
+ }
+
+ public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
+ // evaluate if card size exceeds defined limit
+ $eventSizeLimit = $this->config->getValueInt(Application::APP_ID, 'event_size_limit', 10485760);
+ if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $eventSizeLimit) {
+ throw new Forbidden("VEvent or VTodo object exceeds $eventSizeLimit bytes");
+ }
+ // all tests passed return true
+ return true;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Connection.php b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php
new file mode 100644
index 00000000000..3d12c92c49a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\WebcalCaching;
+
+use Exception;
+use GuzzleHttp\RequestOptions;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\LocalServerException;
+use OCP\IAppConfig;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Reader;
+
+class Connection {
+ public function __construct(
+ private IClientService $clientService,
+ private IAppConfig $config,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * gets webcal feed from remote server
+ */
+ public function queryWebcalFeed(array $subscription): ?string {
+ $subscriptionId = $subscription['id'];
+ $url = $this->cleanURL($subscription['source']);
+ if ($url === null) {
+ return null;
+ }
+
+ $allowLocalAccess = $this->config->getValueString('dav', 'webcalAllowLocalAccess', 'no');
+
+ $params = [
+ 'nextcloud' => [
+ 'allow_local_address' => $allowLocalAccess === 'yes',
+ ],
+ RequestOptions::HEADERS => [
+ 'User-Agent' => 'Nextcloud Webcal Service',
+ 'Accept' => 'text/calendar, application/calendar+json, application/calendar+xml',
+ ],
+ ];
+
+ $user = parse_url($subscription['source'], PHP_URL_USER);
+ $pass = parse_url($subscription['source'], PHP_URL_PASS);
+ if ($user !== null && $pass !== null) {
+ $params[RequestOptions::AUTH] = [$user, $pass];
+ }
+
+ try {
+ $client = $this->clientService->newClient();
+ $response = $client->get($url, $params);
+ } catch (LocalServerException $ex) {
+ $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
+ 'exception' => $ex,
+ ]);
+ return null;
+ } catch (Exception $ex) {
+ $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
+ 'exception' => $ex,
+ ]);
+ return null;
+ }
+
+ $body = $response->getBody();
+
+ $contentType = $response->getHeader('Content-Type');
+ $contentType = explode(';', $contentType, 2)[0];
+ switch ($contentType) {
+ case 'application/calendar+json':
+ try {
+ $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $jCalendar->serialize();
+
+ case 'application/calendar+xml':
+ try {
+ $xCalendar = Reader::readXML($body);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $xCalendar->serialize();
+
+ case 'text/calendar':
+ default:
+ try {
+ $vCalendar = Reader::read($body);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $vCalendar->serialize();
+ }
+ }
+
+ /**
+ * This method will strip authentication information and replace the
+ * 'webcal' or 'webcals' protocol scheme
+ *
+ * @param string $url
+ * @return string|null
+ */
+ private function cleanURL(string $url): ?string {
+ $parsed = parse_url($url);
+ if ($parsed === false) {
+ return null;
+ }
+
+ if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
+ $scheme = 'http';
+ } else {
+ $scheme = 'https';
+ }
+
+ $host = $parsed['host'] ?? '';
+ $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
+ $path = $parsed['path'] ?? '';
+ $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
+ $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
+
+ $cleanURL = "$scheme://$host$port$path$query$fragment";
+ // parse_url is giving some weird results if no url and no :// is given,
+ // so let's test the url again
+ $parsedClean = parse_url($cleanURL);
+ if ($parsedClean === false || !isset($parsedClean['host'])) {
+ return null;
+ }
+
+ return $cleanURL;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
index 8d4714d313a..e07be39c7b4 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
@@ -3,30 +3,12 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\WebcalCaching;
-use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarRoot;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Server;
@@ -41,10 +23,14 @@ class Plugin extends ServerPlugin {
* that do not support subscriptions on their own
*
* /^MSFT-WIN-3/ - Windows 10 Calendar
+ * /Evolution/ - Gnome Calendar/Evolution
+ * /KIO/ - KDE PIM/Akonadi
* @var string[]
*/
public const ENABLE_FOR_CLIENTS = [
- "/^MSFT-WIN-3/"
+ '/^MSFT-WIN-3/',
+ '/Evolution/',
+ '/KIO/'
];
/**
@@ -71,6 +57,11 @@ class Plugin extends ServerPlugin {
if ($magicHeader === 'On') {
$this->enabled = true;
}
+
+ $isExportRequest = $request->getMethod() === 'GET' && array_key_exists('export', $request->getParams());
+ if ($isExportRequest) {
+ $this->enabled = true;
+ }
}
/**
@@ -85,7 +76,7 @@ class Plugin extends ServerPlugin {
*/
public function initialize(Server $server) {
$this->server = $server;
- $server->on('beforeMethod:*', [$this, 'beforeMethod']);
+ $server->on('beforeMethod:*', [$this, 'beforeMethod'], 15);
}
/**
@@ -107,16 +98,11 @@ class Plugin extends ServerPlugin {
return;
}
- // $calendarHomePath will look like: calendars/username
- $calendarHomePath = $pathParts[0] . '/' . $pathParts[1];
try {
- $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
- if (!($calendarHome instanceof CalendarHome)) {
- //how did we end up here?
- return;
+ $calendarRoot = $this->server->tree->getNodeForPath($pathParts[0]);
+ if ($calendarRoot instanceof CalendarRoot) {
+ $calendarRoot->enableReturnCachedSubscriptions($pathParts[1]);
}
-
- $calendarHome->enableCachedSubscriptionsForThisRequest();
} catch (NotFound $ex) {
return;
}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
index 4035f345876..a0981e6dec1 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
@@ -3,46 +3,17 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
- * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author eleith <online+github@eleith.com>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\WebcalCaching;
-use Exception;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Middleware;
use OCA\DAV\CalDAV\CalDavBackend;
-use OCP\Http\Client\IClientService;
-use OCP\Http\Client\LocalServerException;
-use OCP\IConfig;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
+use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\PropPatch;
-use Sabre\DAV\Xml\Property\Href;
use Sabre\VObject\Component;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
@@ -55,25 +26,17 @@ use function count;
class RefreshWebcalService {
- private CalDavBackend $calDavBackend;
-
- private IClientService $clientService;
-
- private IConfig $config;
-
- /** @var LoggerInterface */
- private LoggerInterface $logger;
-
public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
- public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, LoggerInterface $logger) {
- $this->calDavBackend = $calDavBackend;
- $this->clientService = $clientService;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private Connection $connection,
+ private ITimeFactory $time,
+ ) {
}
public function refreshSubscription(string $principalUri, string $uri) {
@@ -83,11 +46,25 @@ class RefreshWebcalService {
return;
}
- $webcalData = $this->queryWebcalFeed($subscription, $mutations);
+ // Check the refresh rate if there is any
+ if (!empty($subscription['{http://apple.com/ns/ical/}refreshrate'])) {
+ // add the refresh interval to the lastmodified timestamp
+ $refreshInterval = new \DateInterval($subscription['{http://apple.com/ns/ical/}refreshrate']);
+ $updateTime = $this->time->getDateTime();
+ $updateTime->setTimestamp($subscription['lastmodified'])->add($refreshInterval);
+ if ($updateTime->getTimestamp() > $this->time->getTime()) {
+ return;
+ }
+ }
+
+
+ $webcalData = $this->connection->queryWebcalFeed($subscription);
if (!$webcalData) {
return;
}
+ $localData = $this->calDavBackend->getLimitedCalendarObjects((int)$subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+
$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
@@ -95,14 +72,10 @@ class RefreshWebcalService {
try {
$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
- // we wait with deleting all outdated events till we parsed the new ones
- // in case the new calendar is broken and `new ICalendar` throws a ParseException
- // the user will still see the old data
- $this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
-
while ($vObject = $splitter->getNext()) {
/** @var Component $vObject */
$compName = null;
+ $uid = null;
foreach ($vObject->getComponents() as $component) {
if ($component->name === 'VTIMEZONE') {
@@ -117,21 +90,68 @@ class RefreshWebcalService {
if ($stripAttachments) {
unset($component->{'ATTACH'});
}
+
+ $uid = $component->{ 'UID' }->getValue();
}
if ($stripTodos && $compName === 'VTODO') {
continue;
}
- $objectUri = $this->getRandomCalendarObjectUri();
- $calendarData = $vObject->serialize();
+ if (!isset($uid)) {
+ continue;
+ }
+
try {
- $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
- } catch (NoInstancesException | BadRequest $ex) {
- $this->logger->error('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
+ $denormalized = $this->calDavBackend->getDenormalizedData($vObject->serialize());
+ } catch (InvalidDataException|Forbidden $ex) {
+ $this->logger->warning('Unable to denormalize calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
+ continue;
+ }
+
+ // Find all identical sets and remove them from the update
+ if (isset($localData[$uid]) && $denormalized['etag'] === $localData[$uid]['etag']) {
+ unset($localData[$uid]);
+ continue;
+ }
+
+ $vObjectCopy = clone $vObject;
+ $identical = isset($localData[$uid]) && $this->compareWithoutDtstamp($vObjectCopy, $localData[$uid]);
+ if ($identical) {
+ unset($localData[$uid]);
+ continue;
+ }
+
+ // Find all modified sets and update them
+ if (isset($localData[$uid]) && $denormalized['etag'] !== $localData[$uid]['etag']) {
+ $this->calDavBackend->updateCalendarObject($subscription['id'], $localData[$uid]['uri'], $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+ unset($localData[$uid]);
+ continue;
+ }
+
+ // Only entirely new events get created here
+ try {
+ $objectUri = $this->getRandomCalendarObjectUri();
+ $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+ } catch (NoInstancesException|BadRequest $ex) {
+ $this->logger->warning('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
}
}
+ $ids = array_map(static function ($dataSet): int {
+ return (int)$dataSet['id'];
+ }, $localData);
+ $uris = array_map(static function ($dataSet): string {
+ return $dataSet['uri'];
+ }, $localData);
+
+ if (!empty($ids) && !empty($uris)) {
+ // Clean up on aisle 5
+ // The only events left over in the $localData array should be those that don't exist upstream
+ // All deleted VObjects from upstream are removed
+ $this->calDavBackend->purgeCachedEventsForSubscription($subscription['id'], $ids, $uris);
+ }
+
$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
if ($newRefreshRate) {
$mutations[self::REFRESH_RATE] = $newRefreshRate;
@@ -139,7 +159,7 @@ class RefreshWebcalService {
$this->updateSubscription($subscription, $mutations);
} catch (ParseException $ex) {
- $this->logger->error("Subscription {subscriptionId} could not be refreshed due to a parsing error", ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
+ $this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
}
}
@@ -161,111 +181,6 @@ class RefreshWebcalService {
return $subscriptions[0];
}
- /**
- * gets webcal feed from remote server
- */
- private function queryWebcalFeed(array $subscription, array &$mutations): ?string {
- $client = $this->clientService->newClient();
-
- $didBreak301Chain = false;
- $latestLocation = null;
-
- $handlerStack = HandlerStack::create();
- $handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
- return $request
- ->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
- ->withHeader('User-Agent', 'Nextcloud Webcal Service');
- }));
- $handlerStack->push(Middleware::mapResponse(function (ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
- if (!$didBreak301Chain) {
- if ($response->getStatusCode() !== 301) {
- $didBreak301Chain = true;
- } else {
- $latestLocation = $response->getHeader('Location');
- }
- }
- return $response;
- }));
-
- $allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
- $subscriptionId = $subscription['id'];
- $url = $this->cleanURL($subscription['source']);
- if ($url === null) {
- return null;
- }
-
- try {
- $params = [
- 'allow_redirects' => [
- 'redirects' => 10
- ],
- 'handler' => $handlerStack,
- 'nextcloud' => [
- 'allow_local_address' => $allowLocalAccess === 'yes',
- ]
- ];
-
- $user = parse_url($subscription['source'], PHP_URL_USER);
- $pass = parse_url($subscription['source'], PHP_URL_PASS);
- if ($user !== null && $pass !== null) {
- $params['auth'] = [$user, $pass];
- }
-
- $response = $client->get($url, $params);
- $body = $response->getBody();
-
- if ($latestLocation) {
- $mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
- }
-
- $contentType = $response->getHeader('Content-Type');
- $contentType = explode(';', $contentType, 2)[0];
- switch ($contentType) {
- case 'application/calendar+json':
- try {
- $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $jCalendar->serialize();
-
- case 'application/calendar+xml':
- try {
- $xCalendar = Reader::readXML($body);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $xCalendar->serialize();
-
- case 'text/calendar':
- default:
- try {
- $vCalendar = Reader::read($body);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $vCalendar->serialize();
- }
- } catch (LocalServerException $ex) {
- $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
- 'exception' => $ex,
- ]);
-
- return null;
- } catch (Exception $ex) {
- $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
- 'exception' => $ex,
- ]);
-
- return null;
- }
- }
/**
* check if:
@@ -326,47 +241,24 @@ class RefreshWebcalService {
}
/**
- * This method will strip authentication information and replace the
- * 'webcal' or 'webcals' protocol scheme
+ * Returns a random uri for a calendar-object
*
- * @param string $url
- * @return string|null
+ * @return string
*/
- private function cleanURL(string $url): ?string {
- $parsed = parse_url($url);
- if ($parsed === false) {
- return null;
- }
+ public function getRandomCalendarObjectUri():string {
+ return UUIDUtil::getUUID() . '.ics';
+ }
- if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
- $scheme = 'http';
- } else {
- $scheme = 'https';
+ private function compareWithoutDtstamp(Component $vObject, array $calendarObject): bool {
+ foreach ($vObject->getComponents() as $component) {
+ unset($component->{'DTSTAMP'});
}
- $host = $parsed['host'] ?? '';
- $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
- $path = $parsed['path'] ?? '';
- $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
- $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
-
- $cleanURL = "$scheme://$host$port$path$query$fragment";
- // parse_url is giving some weird results if no url and no :// is given,
- // so let's test the url again
- $parsedClean = parse_url($cleanURL);
- if ($parsedClean === false || !isset($parsedClean['host'])) {
- return null;
+ $localVobject = Reader::read($calendarObject['calendardata']);
+ foreach ($localVobject->getComponents() as $component) {
+ unset($component->{'DTSTAMP'});
}
- return $cleanURL;
- }
-
- /**
- * Returns a random uri for a calendar-object
- *
- * @return string
- */
- public function getRandomCalendarObjectUri():string {
- return UUIDUtil::getUUID() . '.ics';
+ return strcasecmp($localVobject->serialize(), $vObject->serialize()) === 0;
}
}
diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php
index f61fb5d2f0a..f9bad25bf31 100644
--- a/apps/dav/lib/Capabilities.php
+++ b/apps/dav/lib/Capabilities.php
@@ -1,51 +1,39 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH
- *
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Louis Chemineau <louis@chmn.me>
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV;
use OCP\Capabilities\ICapability;
use OCP\IConfig;
+use OCP\User\IAvailabilityCoordinator;
class Capabilities implements ICapability {
- private IConfig $config;
-
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private IAvailabilityCoordinator $coordinator,
+ ) {
}
/**
- * @return array{dav: array{chunking: string, bulkupload?: string}}
+ * @return array{dav: array{chunking: string, public_shares_chunking: bool, bulkupload?: string, absence-supported?: bool, absence-replacement?: bool}}
*/
public function getCapabilities() {
$capabilities = [
'dav' => [
'chunking' => '1.0',
+ 'public_shares_chunking' => true,
]
];
if ($this->config->getSystemValueBool('bulkupload.enabled', true)) {
$capabilities['dav']['bulkupload'] = '1.0';
}
+ if ($this->coordinator->isEnabled()) {
+ $capabilities['dav']['absence-supported'] = true;
+ $capabilities['dav']['absence-replacement'] = true;
+ }
return $capabilities;
}
}
diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php
index f0a5ee05e82..b08414d3b02 100644
--- a/apps/dav/lib/CardDAV/Activity/Backend.php
+++ b/apps/dav/lib/CardDAV/Activity/Backend.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity;
@@ -39,31 +22,13 @@ use Sabre\VObject\Reader;
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- /** @var IUserManager */
- protected $userManager;
-
- public function __construct(IActivityManager $activityManager,
- IGroupManager $groupManager,
- IUserSession $userSession,
- IAppManager $appManager,
- IUserManager $userManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
- $this->userManager = $userManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -128,7 +93,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -156,7 +121,7 @@ class Backend {
[
'actor' => $currentUser,
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -187,7 +152,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -212,7 +177,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -241,7 +206,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -283,7 +248,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -310,7 +275,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -388,7 +353,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -432,7 +397,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('addressbook', (int) $addressbookData['id'])
+ ->setObject('addressbook', (int)$addressbookData['id'])
->setType('contacts')
->setAuthor($currentUser);
@@ -444,7 +409,7 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'addressbook' => [
- 'id' => (int) $addressbookData['id'],
+ 'id' => (int)$addressbookData['id'],
'uri' => $addressbookData['uri'],
'name' => $addressbookData['{DAV:}displayname'],
],
@@ -471,7 +436,7 @@ class Backend {
*/
protected function getCardNameAndId(array $cardData): array {
$vObject = Reader::read($cardData['carddata']);
- return ['id' => (string) $vObject->UID, 'name' => (string) ($vObject->FN ?? '')];
+ return ['id' => (string)$vObject->UID, 'name' => (string)($vObject->FN ?? '')];
}
/**
diff --git a/apps/dav/lib/CardDAV/Activity/Filter.php b/apps/dav/lib/CardDAV/Activity/Filter.php
index 3ca4c3367d5..8b221a29ff0 100644
--- a/apps/dav/lib/CardDAV/Activity/Filter.php
+++ b/apps/dav/lib/CardDAV/Activity/Filter.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity;
@@ -28,15 +12,10 @@ use OCP\IURLGenerator;
class Filter implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -55,8 +34,8 @@ class Filter implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*/
public function getPriority(): int {
return 40;
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
index a404dde4448..cdb9769401f 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php
@@ -3,28 +3,12 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -43,25 +27,15 @@ class Addressbook extends Base {
public const SUBJECT_UNSHARE_USER = 'addressbook_user_unshare';
public const SUBJECT_UNSHARE_GROUP = 'addressbook_group_unshare';
- /** @var IFactory */
- protected $languageFactory;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- public function __construct(IFactory $languageFactory,
+ public function __construct(
+ protected IFactory $languageFactory,
IURLGenerator $url,
- IManager $activityManager,
+ protected IManager $activityManager,
IUserManager $userManager,
IGroupManager $groupManager,
- IEventMerger $eventMerger) {
+ protected IEventMerger $eventMerger,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
}
/**
@@ -69,11 +43,11 @@ class Addressbook extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$l = $this->languageFactory->get('dav', $language);
@@ -119,7 +93,7 @@ class Addressbook extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') {
$subject = $l->t('{actor} unshared address book {addressbook} from group {group}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event, $l);
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Base.php b/apps/dav/lib/CardDAV/Activity/Provider/Base.php
index f475f9d76b7..ea7680aed60 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity\Provider;
@@ -35,27 +18,17 @@ use OCP\IURLGenerator;
use OCP\IUserManager;
abstract class Base implements IProvider {
- /** @var IUserManager */
- protected $userManager;
-
- /** @var string[] */
+ /** @var string[] */
protected $userDisplayNames = [];
- /** @var IGroupManager */
- protected $groupManager;
-
/** @var string[] */
protected $groupDisplayNames = [];
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IUserManager $userManager,
- IGroupManager $groupManager,
- IURLGenerator $urlGenerator) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->url = $urlGenerator;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ protected IURLGenerator $url,
+ ) {
}
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
@@ -68,18 +41,18 @@ abstract class Base implements IProvider {
* @return array
*/
protected function generateAddressbookParameter(array $data, IL10N $l): array {
- if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI &&
- $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) {
+ if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI
+ && $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) {
return [
'type' => 'addressbook',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'addressbook',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $data['name'],
];
}
diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Card.php b/apps/dav/lib/CardDAV/Activity/Provider/Card.php
index 7f49a428cae..acf23c00531 100644
--- a/apps/dav/lib/CardDAV/Activity/Provider/Card.php
+++ b/apps/dav/lib/CardDAV/Activity/Provider/Card.php
@@ -3,28 +3,12 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -40,30 +24,16 @@ class Card extends Base {
public const SUBJECT_UPDATE = 'card_update';
public const SUBJECT_DELETE = 'card_delete';
- /** @var IFactory */
- protected $languageFactory;
-
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- /** @var IAppManager */
- protected $appManager;
-
- public function __construct(IFactory $languageFactory,
+ public function __construct(
+ protected IFactory $languageFactory,
IURLGenerator $url,
- IManager $activityManager,
+ protected IManager $activityManager,
IUserManager $userManager,
IGroupManager $groupManager,
- IEventMerger $eventMerger,
- IAppManager $appManager) {
+ protected IEventMerger $eventMerger,
+ protected IAppManager $appManager,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
- $this->appManager = $appManager;
}
/**
@@ -71,11 +41,11 @@ class Card extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
if ($event->getApp() !== 'dav' || $event->getType() !== 'contacts') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$l = $this->languageFactory->get('dav', $language);
@@ -99,7 +69,7 @@ class Card extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') {
$subject = $l->t('You updated contact {card} in address book {addressbook}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event, $l);
diff --git a/apps/dav/lib/CardDAV/Activity/Setting.php b/apps/dav/lib/CardDAV/Activity/Setting.php
index a8a83111dde..cc68cf87c83 100644
--- a/apps/dav/lib/CardDAV/Activity/Setting.php
+++ b/apps/dav/lib/CardDAV/Activity/Setting.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Activity;
@@ -44,8 +27,8 @@ class Setting extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*/
public function getPriority(): int {
return 50;
diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php
index 4f589031f06..4d30d507a7d 100644
--- a/apps/dav/lib/CardDAV/AddressBook.php
+++ b/apps/dav/lib/CardDAV/AddressBook.php
@@ -1,33 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
use OCA\DAV\DAV\Sharing\IShareable;
-use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\Server;
@@ -57,8 +37,8 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
parent::__construct($carddavBackend, $addressBookInfo);
- if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME &&
- $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) {
+ if ($this->addressBookInfo['{DAV:}displayname'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME
+ && $this->getName() === CardDavBackend::PERSONAL_ADDRESSBOOK_URI) {
$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Contacts');
}
}
@@ -253,9 +233,6 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
}
public function getChanges($syncToken, $syncLevel, $limit = null) {
- if (!$syncToken && $limit) {
- throw new UnsupportedLimitOnInitialSyncException();
- }
return parent::getChanges($syncToken, $syncLevel, $limit);
}
@@ -269,7 +246,12 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov
}
try {
- return $this->carddavBackend->moveCard($sourceNode->getAddressbookId(), (int)$this->addressBookInfo['id'], $sourceNode->getUri(), $sourceNode->getOwner());
+ return $this->carddavBackend->moveCard(
+ $sourceNode->getAddressbookId(),
+ $sourceNode->getUri(),
+ $this->getResourceId(),
+ $targetName,
+ );
} catch (Exception $e) {
// Avoid injecting LoggerInterface everywhere
Server::get(LoggerInterface::class)->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php
index 79720429479..ae77498539b 100644
--- a/apps/dav/lib/CardDAV/AddressBookImpl.php
+++ b/apps/dav/lib/CardDAV/AddressBookImpl.php
@@ -1,58 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arne Hamann <kontakt+github@arne.email>
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author call-me-matt <nextcloud@matthiasheinisch.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Constants;
-use OCP\IAddressBook;
+use OCP\IAddressBookEnabled;
use OCP\IURLGenerator;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\UUIDUtil;
-class AddressBookImpl implements IAddressBook {
-
- /** @var CardDavBackend */
- private $backend;
-
- /** @var array */
- private $addressBookInfo;
-
- /** @var AddressBook */
- private $addressBook;
-
- /** @var IURLGenerator */
- private $urlGenerator;
+class AddressBookImpl implements IAddressBookEnabled {
/**
* AddressBookImpl constructor.
@@ -63,14 +27,13 @@ class AddressBookImpl implements IAddressBook {
* @param IUrlGenerator $urlGenerator
*/
public function __construct(
- AddressBook $addressBook,
- array $addressBookInfo,
- CardDavBackend $backend,
- IURLGenerator $urlGenerator) {
- $this->addressBook = $addressBook;
- $this->addressBookInfo = $addressBookInfo;
- $this->backend = $backend;
- $this->urlGenerator = $urlGenerator;
+ private AddressBook $addressBook,
+ private array $addressBookInfo,
+ private CardDavBackend $backend,
+ private IURLGenerator $urlGenerator,
+ private PropertyMapper $propertyMapper,
+ private ?string $userId,
+ ) {
}
/**
@@ -103,19 +66,19 @@ class AddressBookImpl implements IAddressBook {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options Options to define the output format and search behavior
- * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
- * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
- * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
- * - 'limit' - Set a numeric limit for the search results
- * - 'offset' - Set the offset for the limited search results
- * - 'wildcard' - Whether the search should use wildcards
+ * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
+ * example: ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['type => 'HOME', 'value' => 'g@h.i']]
+ * - 'escape_like_param' - If set to false wildcards _ and % are not escaped
+ * - 'limit' - Set a numeric limit for the search results
+ * - 'offset' - Set the offset for the limited search results
+ * - 'wildcard' - Whether the search should use wildcards
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
* @return array an array of contacts which are arrays of key-value-pairs
- * example result:
- * [
- * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'],
- * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']]
- * ]
+ * example result:
+ * [
+ * ['id' => 0, 'FN' => 'Thomas Müller', 'EMAIL' => 'a@b.c', 'GEO' => '37.386013;-122.082932'],
+ * ['id' => 5, 'FN' => 'Thomas Tanghus', 'EMAIL' => ['d@e.f', 'g@h.i']]
+ * ]
* @since 5.0.0
*/
public function search($pattern, $searchProperties, $options) {
@@ -156,13 +119,13 @@ class AddressBookImpl implements IAddressBook {
if (is_string($entry)) {
$property = $vCard->createProperty($key, $entry);
} else {
- if (($key === "ADR" || $key === "PHOTO") && is_string($entry["value"])) {
- $entry["value"] = stripslashes($entry["value"]);
- $entry["value"] = explode(';', $entry["value"]);
+ if (($key === 'ADR' || $key === 'PHOTO') && is_string($entry['value'])) {
+ $entry['value'] = stripslashes($entry['value']);
+ $entry['value'] = explode(';', $entry['value']);
}
- $property = $vCard->createProperty($key, $entry["value"]);
- if (isset($entry["type"])) {
- $property->add('TYPE', $entry["type"]);
+ $property = $vCard->createProperty($key, $entry['value']);
+ if (isset($entry['type'])) {
+ $property->add('TYPE', $entry['type']);
}
}
$vCard->add($property);
@@ -189,6 +152,10 @@ class AddressBookImpl implements IAddressBook {
$permissions = $this->addressBook->getACL();
$result = 0;
foreach ($permissions as $permission) {
+ if ($this->addressBookInfo['principaluri'] !== $permission['principal']) {
+ continue;
+ }
+
switch ($permission['privilege']) {
case '{DAV:}read':
$result |= Constants::PERMISSION_READ;
@@ -344,8 +311,29 @@ class AddressBookImpl implements IAddressBook {
*/
public function isSystemAddressBook(): bool {
return $this->addressBookInfo['principaluri'] === 'principals/system/system' && (
- $this->addressBookInfo['uri'] === 'system' ||
- $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl()
+ $this->addressBookInfo['uri'] === 'system'
+ || $this->addressBookInfo['{DAV:}displayname'] === $this->urlGenerator->getBaseUrl()
);
}
+
+ public function isEnabled(): bool {
+ if (!$this->userId) {
+ return true;
+ }
+
+ if ($this->isSystemAddressBook()) {
+ $user = $this->userId ;
+ $uri = 'z-server-generated--system';
+ } else {
+ $user = str_replace('principals/users/', '', $this->addressBookInfo['principaluri']);
+ $uri = $this->addressBookInfo['uri'];
+ }
+
+ $path = 'addressbooks/users/' . $user . '/' . $uri;
+ $properties = $this->propertyMapper->findPropertyByPathAndName($user, $path, '{http://owncloud.org/ns}enabled');
+ if (count($properties) > 0) {
+ return (bool)$properties[0]->getPropertyvalue();
+ }
+ return true;
+ }
}
diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php
index f450dbe2ef9..5679a03545e 100644
--- a/apps/dav/lib/CardDAV/AddressBookRoot.php
+++ b/apps/dav/lib/CardDAV/AddressBookRoot.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
@@ -30,26 +13,20 @@ use OCP\IUser;
class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot {
- /** @var PluginManager */
- private $pluginManager;
- private ?IUser $user;
- private ?IGroupManager $groupManager;
-
/**
* @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend
* @param \Sabre\CardDAV\Backend\BackendInterface $carddavBackend
* @param string $principalPrefix
*/
- public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
+ public function __construct(
+ \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend,
\Sabre\CardDAV\Backend\BackendInterface $carddavBackend,
- PluginManager $pluginManager,
- ?IUser $user,
- ?IGroupManager $groupManager,
- string $principalPrefix = 'principals') {
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ string $principalPrefix = 'principals',
+ ) {
parent::__construct($principalBackend, $carddavBackend, $principalPrefix);
- $this->pluginManager = $pluginManager;
- $this->user = $user;
- $this->groupManager = $groupManager;
}
/**
diff --git a/apps/dav/lib/CardDAV/Card.php b/apps/dav/lib/CardDAV/Card.php
index 093255392e0..8cd4fd7e5ee 100644
--- a/apps/dav/lib/CardDAV/Card.php
+++ b/apps/dav/lib/CardDAV/Card.php
@@ -3,30 +3,14 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023, Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
class Card extends \Sabre\CardDAV\Card {
public function getId(): int {
- return (int) $this->cardData['id'];
+ return (int)$this->cardData['id'];
}
public function getUri(): string {
diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php
index f887e3b32b7..a78686eb61d 100644
--- a/apps/dav/lib/CardDAV/CardDavBackend.php
+++ b/apps/dav/lib/CardDAV/CardDavBackend.php
@@ -1,37 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arne Hamann <kontakt+github@arne.email>
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Chih-Hsuan Yen <yan12125@gmail.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author matt <34400929+call-me-matt@users.noreply.github.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
@@ -51,6 +23,7 @@ use OCP\AppFramework\Db\TTransactional;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUserManager;
use PDO;
@@ -87,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
private IUserManager $userManager,
private IEventDispatcher $dispatcher,
private Sharing\Backend $sharingBackend,
+ private IConfig $config,
) {
}
@@ -104,7 +78,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
$result = $query->executeQuery();
- $column = (int) $result->fetchOne();
+ $column = (int)$result->fetchOne();
$result->closeCursor();
return $column;
}
@@ -155,7 +129,6 @@ class CardDavBackend implements BackendInterface, SyncSupport {
// query for shared addressbooks
$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
$principals[] = $principalUri;
@@ -188,8 +161,8 @@ class CardDavBackend implements BackendInterface, SyncSupport {
// New share can not have more permissions then the old one.
continue;
}
- if (isset($addressBooks[$row['id']][$readOnlyPropertyName]) &&
- $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
+ if (isset($addressBooks[$row['id']][$readOnlyPropertyName])
+ && $addressBooks[$row['id']][$readOnlyPropertyName] === 0) {
// Old share is already read-write, no more permissions can be gained
continue;
}
@@ -228,7 +201,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$addressBooks = [];
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$addressBooks[$row['id']] = [
'id' => $row['id'],
@@ -358,7 +331,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)))
->executeStatement();
- $this->addChange($addressBookId, "", 2);
+ $this->addChange($addressBookId, '', 2);
$addressBookRow = $this->getAddressBookById((int)$addressBookId);
$shares = $this->getShares((int)$addressBookId);
@@ -379,6 +352,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param array $properties
* @return int
* @throws BadRequest
+ * @throws Exception
*/
public function createAddressBook($principalUri, $url, array $properties) {
if (strlen($url) > 255) {
@@ -423,7 +397,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'synctoken' => $query->createParameter('synctoken'),
])
->setParameters($values)
- ->execute();
+ ->executeStatement();
$addressBookId = $query->getLastInsertId();
return [
@@ -444,7 +418,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return void
*/
public function deleteAddressBook($addressBookId) {
- $this->atomic(function () use ($addressBookId) {
+ $this->atomic(function () use ($addressBookId): void {
$addressBookId = (int)$addressBookId;
$addressBookData = $this->getAddressBookById($addressBookId);
$shares = $this->getShares($addressBookId);
@@ -501,13 +475,13 @@ class CardDavBackend implements BackendInterface, SyncSupport {
*/
public function getCards($addressbookId) {
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressbookId)));
$cards = [];
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
@@ -538,13 +512,13 @@ class CardDavBackend implements BackendInterface, SyncSupport {
*/
public function getCard($addressBookId, $cardUri) {
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->setMaxResults(1);
- $result = $query->execute();
+ $result = $query->executeQuery();
$row = $result->fetch();
if (!$row) {
return false;
@@ -581,14 +555,14 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$cards = [];
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
+ $query->select(['id', 'addressbookid', 'uri', 'lastmodified', 'etag', 'size', 'carddata', 'uid'])
->from($this->dbCardsTable)
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
->andWhere($query->expr()->in('uri', $query->createParameter('uri')));
foreach ($chunks as $uris) {
$query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
$row['etag'] = '"' . $row['etag'] . '"';
@@ -662,7 +636,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'etag' => $query->createNamedParameter($etag),
'uid' => $query->createNamedParameter($uid),
])
- ->execute();
+ ->executeStatement();
$etagCacheKey = "$addressBookId#$cardUri";
$this->etagCache[$etagCacheKey] = $etag;
@@ -725,7 +699,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->set('uid', $query->createNamedParameter($uid))
->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))
- ->execute();
+ ->executeStatement();
$this->etagCache[$etagCacheKey] = $etag;
@@ -743,32 +717,33 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* @throws Exception
*/
- public function moveCard(int $sourceAddressBookId, int $targetAddressBookId, string $cardUri, string $oldPrincipalUri): bool {
- return $this->atomic(function () use ($sourceAddressBookId, $targetAddressBookId, $cardUri, $oldPrincipalUri) {
- $card = $this->getCard($sourceAddressBookId, $cardUri);
+ public function moveCard(int $sourceAddressBookId, string $sourceObjectUri, int $targetAddressBookId, string $tragetObjectUri): bool {
+ return $this->atomic(function () use ($sourceAddressBookId, $sourceObjectUri, $targetAddressBookId, $tragetObjectUri) {
+ $card = $this->getCard($sourceAddressBookId, $sourceObjectUri);
if (empty($card)) {
return false;
}
+ $sourceObjectId = (int)$card['id'];
$query = $this->db->getQueryBuilder();
$query->update('cards')
->set('addressbookid', $query->createNamedParameter($targetAddressBookId, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->eq('uri', $query->createNamedParameter($cardUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
+ ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($sourceObjectUri, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($sourceAddressBookId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->executeStatement();
- $this->purgeProperties($sourceAddressBookId, (int)$card['id']);
- $this->updateProperties($sourceAddressBookId, $card['uri'], $card['carddata']);
+ $this->purgeProperties($sourceAddressBookId, $sourceObjectId);
+ $this->updateProperties($targetAddressBookId, $tragetObjectUri, $card['carddata']);
- $this->addChange($sourceAddressBookId, $card['uri'], 3);
- $this->addChange($targetAddressBookId, $card['uri'], 1);
+ $this->addChange($sourceAddressBookId, $sourceObjectUri, 3);
+ $this->addChange($targetAddressBookId, $tragetObjectUri, 1);
- $card = $this->getCard($targetAddressBookId, $cardUri);
+ $card = $this->getCard($targetAddressBookId, $tragetObjectUri);
// Card wasn't found - possibly because it was deleted in the meantime by a different client
if (empty($card)) {
return false;
}
-
$targetAddressBookRow = $this->getAddressBookById($targetAddressBookId);
// the address book this card is being moved to does not exist any longer
if (empty($targetAddressBookRow)) {
@@ -878,6 +853,8 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return array
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) {
+ $maxLimit = $this->config->getSystemValueInt('carddav_sync_request_truncation', 2500);
+ $limit = ($limit === null) ? $maxLimit : min($limit, $maxLimit);
// Current synctoken
return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) {
$qb = $this->db->getQueryBuilder();
@@ -900,10 +877,35 @@ class CardDavBackend implements BackendInterface, SyncSupport {
'modified' => [],
'deleted' => [],
];
-
- if ($syncToken) {
+ if (str_starts_with($syncToken, 'init_')) {
+ $syncValues = explode('_', $syncToken);
+ $lastID = $syncValues[1];
+ $initialSyncToken = $syncValues[2];
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id', 'uri')
+ ->from('cards')
+ ->where(
+ $qb->expr()->andX(
+ $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)),
+ $qb->expr()->gt('id', $qb->createNamedParameter($lastID)))
+ )->orderBy('id')
+ ->setMaxResults($limit);
+ $stmt = $qb->executeQuery();
+ $values = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
+ if (count($values) === 0) {
+ $result['syncToken'] = $initialSyncToken;
+ $result['result_truncated'] = false;
+ $result['added'] = [];
+ } else {
+ $lastID = $values[array_key_last($values)]['id'];
+ $result['added'] = array_column($values, 'uri');
+ $result['syncToken'] = count($result['added']) >= $limit ? "init_{$lastID}_$initialSyncToken" : $initialSyncToken ;
+ $result['result_truncated'] = count($result['added']) >= $limit;
+ }
+ } elseif ($syncToken) {
$qb = $this->db->getQueryBuilder();
- $qb->select('uri', 'operation')
+ $qb->select('uri', 'operation', 'synctoken')
->from('addressbookchanges')
->where(
$qb->expr()->andX(
@@ -913,22 +915,31 @@ class CardDavBackend implements BackendInterface, SyncSupport {
)
)->orderBy('synctoken');
- if (is_int($limit) && $limit > 0) {
+ if ($limit > 0) {
$qb->setMaxResults($limit);
}
// Fetching all changes
$stmt = $qb->executeQuery();
+ $rowCount = $stmt->rowCount();
$changes = [];
+ $highestSyncToken = 0;
// This loop ensures that any duplicates are overwritten, only the
// last change on a node is relevant.
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$changes[$row['uri']] = $row['operation'];
+ $highestSyncToken = $row['synctoken'];
}
+
$stmt->closeCursor();
+ // No changes found, use current token
+ if (empty($changes)) {
+ $result['syncToken'] = $currentToken;
+ }
+
foreach ($changes as $uri => $operation) {
switch ($operation) {
case 1:
@@ -942,16 +953,43 @@ class CardDavBackend implements BackendInterface, SyncSupport {
break;
}
}
+
+ /*
+ * The synctoken in oc_addressbooks is always the highest synctoken in oc_addressbookchanges for a given addressbook plus one (see addChange).
+ *
+ * For truncated results, it is expected that we return the highest token from the response, so the client can continue from the latest change.
+ *
+ * For non-truncated results, it is expected to return the currentToken. If we return the highest token, as with truncated results, the client will always think it is one change behind.
+ *
+ * Therefore, we differentiate between truncated and non-truncated results when returning the synctoken.
+ */
+ if ($rowCount === $limit && $highestSyncToken < $currentToken) {
+ $result['syncToken'] = $highestSyncToken;
+ $result['result_truncated'] = true;
+ }
} else {
$qb = $this->db->getQueryBuilder();
- $qb->select('uri')
+ $qb->select('id', 'uri')
->from('cards')
->where(
$qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId))
);
// No synctoken supplied, this is the initial sync.
+ $qb->setMaxResults($limit);
$stmt = $qb->executeQuery();
- $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ $values = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ if (empty($values)) {
+ $result['added'] = [];
+ return $result;
+ }
+ $lastID = $values[array_key_last($values)]['id'];
+ if (count($values) >= $limit) {
+ $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken;
+ $result['result_truncated'] = true;
+ }
+
+ $result['added'] = array_column($values, 'uri');
+
$stmt->closeCursor();
}
return $result;
@@ -967,7 +1005,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @return void
*/
protected function addChange(int $addressBookId, string $objectUri, int $operation): void {
- $this->atomic(function () use ($addressBookId, $objectUri, $operation) {
+ $this->atomic(function () use ($addressBookId, $objectUri, $operation): void {
$query = $this->db->getQueryBuilder();
$query->select('synctoken')
->from('addressbooks')
@@ -1042,7 +1080,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param list<string> $remove
*/
public function updateShares(IShareable $shareable, array $add, array $remove): void {
- $this->atomic(function () use ($shareable, $add, $remove) {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
$addressBookId = $shareable->getResourceId();
$addressBookData = $this->getAddressBookById($addressBookId);
$oldShares = $this->getShares($addressBookId);
@@ -1060,11 +1098,11 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options = array() to define the search behavior
- * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
- * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
- * - 'limit' - Set a numeric limit for the search results
- * - 'offset' - Set the offset for the limited search results
- * - 'wildcard' - Whether the search should use wildcards
+ * - 'types' boolean (since 15.0.0) If set to true, fields that come with a TYPE property will be an array
+ * - 'escape_like_param' - If set to false wildcards _ and % are not escaped, otherwise they are
+ * - 'limit' - Set a numeric limit for the search results
+ * - 'offset' - Set the offset for the limited search results
+ * - 'wildcard' - Whether the search should use wildcards
* @psalm-param array{types?: bool, escape_like_param?: bool, limit?: int, offset?: int, wildcard?: bool} $options
* @return array an array of contacts which are arrays of key-value-pairs
*/
@@ -1089,7 +1127,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
array $options = []): array {
return $this->atomic(function () use ($principalUri, $pattern, $searchProperties, $options) {
$addressBookIds = array_map(static function ($row):int {
- return (int) $row['id'];
+ return (int)$row['id'];
}, $this->getAddressBooksForUser($principalUri));
return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
@@ -1176,24 +1214,24 @@ class CardDavBackend implements BackendInterface, SyncSupport {
/**
* FIXME Find a way to match only 4 last digits
* BDAY can be --1018 without year or 20001019 with it
- * $bDayOr = $query2->expr()->orX();
+ * $bDayOr = [];
* if ($options['since'] instanceof DateTimeFilter) {
- * $bDayOr->add(
+ * $bDayOr[] =
* $query2->expr()->gte('SUBSTR(cp_bday.value, -4)',
- * $query2->createNamedParameter($options['since']->get()->format('md')))
+ * $query2->createNamedParameter($options['since']->get()->format('md'))
* );
* }
* if ($options['until'] instanceof DateTimeFilter) {
- * $bDayOr->add(
+ * $bDayOr[] =
* $query2->expr()->lte('SUBSTR(cp_bday.value, -4)',
- * $query2->createNamedParameter($options['until']->get()->format('md')))
+ * $query2->createNamedParameter($options['until']->get()->format('md'))
* );
* }
- * $query2->andWhere($bDayOr);
+ * $query2->andWhere($query2->expr()->orX(...$bDayOr));
*/
}
- $result = $query2->execute();
+ $result = $query2->executeQuery();
$matches = $result->fetchAll();
$result->closeCursor();
$matches = array_map(function ($match) {
@@ -1214,7 +1252,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
return array_map(function ($array) {
- $array['addressbookid'] = (int) $array['addressbookid'];
+ $array['addressbookid'] = (int)$array['addressbookid'];
$modified = false;
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
if ($modified) {
@@ -1235,7 +1273,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->from($this->dbCardsPropertiesTable)
->where($query->expr()->eq('name', $query->createNamedParameter($name)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($bookId)))
- ->execute();
+ ->executeQuery();
$all = $result->fetchAll(PDO::FETCH_COLUMN);
$result->closeCursor();
@@ -1255,7 +1293,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('id', $query->createParameter('id')))
->setParameter('id', $id);
- $result = $query->execute();
+ $result = $query->executeQuery();
$uri = $result->fetch();
$result->closeCursor();
@@ -1279,7 +1317,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->select('*')->from($this->dbCardsTable)
->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $queryResult = $query->execute();
+ $queryResult = $query->executeQuery();
$contact = $queryResult->fetch();
$queryResult->closeCursor();
@@ -1320,7 +1358,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* @param string $vCardSerialized
*/
protected function updateProperties($addressBookId, $cardUri, $vCardSerialized) {
- $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized) {
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCardSerialized): void {
$cardId = $this->getCardId($addressBookId, $cardUri);
$vCard = $this->readCard($vCardSerialized);
@@ -1352,7 +1390,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->setParameter('name', $property->name);
$query->setParameter('value', mb_strcut($property->getValue(), 0, 254));
$query->setParameter('preferred', $preferred);
- $query->execute();
+ $query->executeStatement();
}
}, $this->db);
}
@@ -1378,7 +1416,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$query->delete($this->dbCardsPropertiesTable)
->where($query->expr()->eq('cardid', $query->createNamedParameter($cardId)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $query->execute();
+ $query->executeStatement();
}
/**
@@ -1390,7 +1428,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
- $result = $query->execute();
+ $result = $query->executeQuery();
$cardIds = $result->fetch();
$result->closeCursor();
@@ -1426,7 +1464,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
->from('addressbookchanges');
$result = $query->executeQuery();
- $maxId = (int) $result->fetchOne();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
if (!$maxId || $maxId < $keep) {
return 0;
diff --git a/apps/dav/lib/CardDAV/ContactsManager.php b/apps/dav/lib/CardDAV/ContactsManager.php
index bed1e676337..b35137c902d 100644
--- a/apps/dav/lib/CardDAV/ContactsManager.php
+++ b/apps/dav/lib/CardDAV/ContactsManager.php
@@ -1,50 +1,29 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobia De Koninck <tobia@ledfan.be>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Contacts\IManager;
use OCP\IL10N;
use OCP\IURLGenerator;
class ContactsManager {
- /** @var CardDavBackend */
- private $backend;
-
- /** @var IL10N */
- private $l10n;
-
/**
* ContactsManager constructor.
*
* @param CardDavBackend $backend
* @param IL10N $l10n
*/
- public function __construct(CardDavBackend $backend, IL10N $l10n) {
- $this->backend = $backend;
- $this->l10n = $l10n;
+ public function __construct(
+ private CardDavBackend $backend,
+ private IL10N $l10n,
+ private PropertyMapper $propertyMapper,
+ ) {
}
/**
@@ -54,33 +33,37 @@ class ContactsManager {
*/
public function setupContactsProvider(IManager $cm, $userId, IURLGenerator $urlGenerator) {
$addressBooks = $this->backend->getAddressBooksForUser("principals/users/$userId");
- $this->register($cm, $addressBooks, $urlGenerator);
- $this->setupSystemContactsProvider($cm, $urlGenerator);
+ $this->register($cm, $addressBooks, $urlGenerator, $userId);
+ $this->setupSystemContactsProvider($cm, $userId, $urlGenerator);
}
/**
* @param IManager $cm
+ * @param ?string $userId
* @param IURLGenerator $urlGenerator
*/
- public function setupSystemContactsProvider(IManager $cm, IURLGenerator $urlGenerator) {
- $addressBooks = $this->backend->getAddressBooksForUser("principals/system/system");
- $this->register($cm, $addressBooks, $urlGenerator);
+ public function setupSystemContactsProvider(IManager $cm, ?string $userId, IURLGenerator $urlGenerator) {
+ $addressBooks = $this->backend->getAddressBooksForUser('principals/system/system');
+ $this->register($cm, $addressBooks, $urlGenerator, $userId);
}
/**
* @param IManager $cm
* @param $addressBooks
* @param IURLGenerator $urlGenerator
+ * @param ?string $userId
*/
- private function register(IManager $cm, $addressBooks, $urlGenerator) {
+ private function register(IManager $cm, $addressBooks, $urlGenerator, ?string $userId) {
foreach ($addressBooks as $addressBookInfo) {
- $addressBook = new \OCA\DAV\CardDAV\AddressBook($this->backend, $addressBookInfo, $this->l10n);
+ $addressBook = new AddressBook($this->backend, $addressBookInfo, $this->l10n);
$cm->registerAddressBook(
new AddressBookImpl(
$addressBook,
$addressBookInfo,
$this->backend,
- $urlGenerator
+ $urlGenerator,
+ $this->propertyMapper,
+ $userId,
)
);
}
diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php
index 8ea75fbef74..30dba99839e 100644
--- a/apps/dav/lib/CardDAV/Converter.php
+++ b/apps/dav/lib/CardDAV/Converter.php
@@ -1,53 +1,31 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
+use DateTimeImmutable;
use Exception;
use OCP\Accounts\IAccountManager;
use OCP\IImage;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
+use Sabre\VObject\Property\VCard\Date;
class Converter {
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var IAccountManager */
- private $accountManager;
- private IUserManager $userManager;
-
- public function __construct(IAccountManager $accountManager,
- IUserManager $userManager, IURLGenerator $urlGenerator) {
- $this->accountManager = $accountManager;
- $this->userManager = $userManager;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ private IAccountManager $accountManager,
+ private IUserManager $userManager,
+ private IURLGenerator $urlGenerator,
+ private LoggerInterface $logger,
+ ) {
}
public function createCardFromUser(IUser $user): ?VCard {
@@ -98,7 +76,7 @@ class Converter {
new Text(
$vCard,
'X-SOCIALPROFILE',
- $this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $user->getUID()]),
+ $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]),
[
'TYPE' => 'NEXTCLOUD',
'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED
@@ -134,6 +112,24 @@ class Converter {
case IAccountManager::PROPERTY_ROLE:
$vCard->add(new Text($vCard, 'TITLE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
break;
+ case IAccountManager::PROPERTY_BIOGRAPHY:
+ $vCard->add(new Text($vCard, 'NOTE', $property->getValue(), ['X-NC-SCOPE' => $scope]));
+ break;
+ case IAccountManager::PROPERTY_BIRTHDATE:
+ try {
+ $birthdate = new DateTimeImmutable($property->getValue());
+ } catch (Exception $e) {
+ // Invalid date -> just skip the property
+ $this->logger->info("Failed to parse user's birthdate for the SAB: " . $property->getValue(), [
+ 'exception' => $e,
+ 'userId' => $user->getUID(),
+ ]);
+ break;
+ }
+ $dateProperty = new Date($vCard, 'BDAY', null, ['X-NC-SCOPE' => $scope]);
+ $dateProperty->setDateTime($birthdate);
+ $vCard->add($dateProperty);
+ break;
}
}
diff --git a/apps/dav/lib/CardDAV/HasPhotoPlugin.php b/apps/dav/lib/CardDAV/HasPhotoPlugin.php
index 310649bdae9..6e2e0423910 100644
--- a/apps/dav/lib/CardDAV/HasPhotoPlugin.php
+++ b/apps/dav/lib/CardDAV/HasPhotoPlugin.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV;
diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php
index 3ebc91e5533..74a8b032e42 100644
--- a/apps/dav/lib/CardDAV/ImageExportPlugin.php
+++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php
@@ -1,29 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jacob Neplokh <me@jacobneplokh.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
+use OCP\AppFramework\Http;
use OCP\Files\NotFoundException;
use Sabre\CardDAV\Card;
use Sabre\DAV\Server;
@@ -35,16 +19,15 @@ class ImageExportPlugin extends ServerPlugin {
/** @var Server */
protected $server;
- /** @var PhotoCache */
- private $cache;
/**
* ImageExportPlugin constructor.
*
* @param PhotoCache $cache
*/
- public function __construct(PhotoCache $cache) {
- $this->cache = $cache;
+ public function __construct(
+ private PhotoCache $cache,
+ ) {
}
/**
@@ -77,7 +60,7 @@ class ImageExportPlugin extends ServerPlugin {
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
- if (!($node instanceof Card)) {
+ if (!$node instanceof Card) {
return true;
}
@@ -104,11 +87,11 @@ class ImageExportPlugin extends ServerPlugin {
$response->setHeader('Content-Type', $file->getMimeType());
$fileName = $node->getName() . '.' . PhotoCache::ALLOWED_CONTENT_TYPES[$file->getMimeType()];
$response->setHeader('Content-Disposition', "attachment; filename=$fileName");
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($file->getContent());
} catch (NotFoundException $e) {
- $response->setStatus(404);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
}
return false;
diff --git a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
index 6f05bb6ce6a..372906a6ae8 100644
--- a/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
+++ b/apps/dav/lib/CardDAV/Integration/ExternalAddressBook.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Integration;
@@ -50,12 +32,10 @@ abstract class ExternalAddressBook implements IAddressBook, DAV\IProperties {
*/
private const DELIMITER = '--';
- private string $appId;
- private string $uri;
-
- public function __construct(string $appId, string $uri) {
- $this->appId = $appId;
- $this->uri = $uri;
+ public function __construct(
+ private string $appId,
+ private string $uri,
+ ) {
}
/**
diff --git a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php
index 0560c13c05c..a8fa074f635 100644
--- a/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php
+++ b/apps/dav/lib/CardDAV/Integration/IAddressBookProvider.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Integration;
diff --git a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
index a56faad8413..9d6b0df838e 100644
--- a/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
+++ b/apps/dav/lib/CardDAV/MultiGetExportPlugin.php
@@ -3,29 +3,12 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV;
+use OCP\AppFramework\Http;
use Sabre\DAV;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
@@ -61,7 +44,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin {
}
// Only handling xml
- $contentType = (string) $response->getHeader('Content-Type');
+ $contentType = (string)$response->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -83,7 +66,7 @@ class MultiGetExportPlugin extends DAV\ServerPlugin {
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
$response->setHeader('Content-Type', 'text/vcard');
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($output);
return true;
diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php
index 9f05ec2354a..03c71f7e4a3 100644
--- a/apps/dav/lib/CardDAV/PhotoCache.php
+++ b/apps/dav/lib/CardDAV/PhotoCache.php
@@ -1,39 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Jacob Neplokh <me@jacobneplokh.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
namespace OCA\DAV\CardDAV;
+use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\Image;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\Card;
use Sabre\VObject\Document;
@@ -42,24 +22,22 @@ use Sabre\VObject\Property\Binary;
use Sabre\VObject\Reader;
class PhotoCache {
+ private ?IAppData $photoCacheAppData = null;
- /** @var array */
+ /** @var array */
public const ALLOWED_CONTENT_TYPES = [
'image/png' => 'png',
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'image/vnd.microsoft.icon' => 'ico',
+ 'image/webp' => 'webp',
+ 'image/avif' => 'avif',
];
- protected IAppData $appData;
- protected LoggerInterface $logger;
-
- /**
- * PhotoCache constructor.
- */
- public function __construct(IAppData $appData, LoggerInterface $logger) {
- $this->appData = $appData;
- $this->logger = $logger;
+ public function __construct(
+ private IAppDataFactory $appDataFactory,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -133,7 +111,7 @@ class PhotoCache {
throw new NotFoundException;
}
- $photo = new \OCP\Image();
+ $photo = new Image();
/** @var ISimpleFile $file */
$file = $folder->getFile('photo.' . $ext);
$photo->loadFromData($file->getContent());
@@ -143,7 +121,7 @@ class PhotoCache {
$ratio = 1 / $ratio;
}
- $size = (int) ($size * $ratio);
+ $size = (int)($size * $ratio);
if ($size !== -1) {
$photo->resize($size);
}
@@ -165,13 +143,12 @@ class PhotoCache {
private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder {
$hash = md5($addressBookId . ' ' . $cardUri);
try {
- return $this->appData->getFolder($hash);
+ return $this->getPhotoCacheAppData()->getFolder($hash);
} catch (NotFoundException $e) {
if ($createIfNotExists) {
- return $this->appData->newFolder($hash);
- } else {
- throw $e;
+ return $this->getPhotoCacheAppData()->newFolder($hash);
}
+ throw $e;
}
}
@@ -264,7 +241,7 @@ class PhotoCache {
if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
/** @var Parameter $typeParam */
$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
- $type = (string) $typeParam->getValue();
+ $type = (string)$typeParam->getValue();
if (str_starts_with($type, 'image/')) {
return $type;
@@ -288,4 +265,11 @@ class PhotoCache {
// that's OK, nothing to do
}
}
+
+ private function getPhotoCacheAppData(): IAppData {
+ if ($this->photoCacheAppData === null) {
+ $this->photoCacheAppData = $this->appDataFactory->get('dav-photocache');
+ }
+ return $this->photoCacheAppData;
+ }
}
diff --git a/apps/dav/lib/CardDAV/Plugin.php b/apps/dav/lib/CardDAV/Plugin.php
index df8f7e6a436..0ec10306ceb 100644
--- a/apps/dav/lib/CardDAV/Plugin.php
+++ b/apps/dav/lib/CardDAV/Plugin.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
diff --git a/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
new file mode 100644
index 00000000000..3e18a1341b0
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CardDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\ServerPlugin;
+use function count;
+use function explode;
+
+class CardDavRateLimitingPlugin extends ServerPlugin {
+ public function __construct(
+ private Limiter $limiter,
+ private IUserManager $userManager,
+ private CardDavBackend $cardDavBackend,
+ private LoggerInterface $logger,
+ private IAppConfig $config,
+ private ?string $userId,
+ ) {
+ $this->limiter = $limiter;
+ $this->userManager = $userManager;
+ $this->cardDavBackend = $cardDavBackend;
+ $this->config = $config;
+ $this->logger = $logger;
+ }
+
+ public function initialize(DAV\Server $server): void {
+ $server->on('beforeBind', [$this, 'beforeBind'], 1);
+ }
+
+ public function beforeBind(string $path): void {
+ if ($this->userId === null) {
+ // We only care about authenticated users here
+ return;
+ }
+ $user = $this->userManager->get($this->userId);
+ if ($user === null) {
+ // We only care about authenticated users here
+ return;
+ }
+
+ $pathParts = explode('/', $path);
+ if (count($pathParts) === 4 && $pathParts[0] === 'addressbooks') {
+ // Path looks like addressbooks/users/username/addressbooksname so a new addressbook is created
+ try {
+ $this->limiter->registerUserRequest(
+ 'carddav-create-address-book',
+ $this->config->getValueInt('dav', 'rateLimitAddressBookCreation', 10),
+ $this->config->getValueInt('dav', 'rateLimitPeriodAddressBookCreation', 3600),
+ $user
+ );
+ } catch (RateLimitExceededException $e) {
+ throw new TooManyRequests('Too many addressbooks created', 0, $e);
+ }
+
+ $addressBookLimit = $this->config->getValueInt('dav', 'maximumAdressbooks', 10);
+ if ($addressBookLimit === -1) {
+ return;
+ }
+ $numAddressbooks = $this->cardDavBackend->getAddressBooksForUserCount('principals/users/' . $user->getUID());
+
+ if ($numAddressbooks >= $addressBookLimit) {
+ $this->logger->warning('Maximum number of address books reached', [
+ 'addressbooks' => $numAddressbooks,
+ 'addressBookLimit' => $addressBookLimit,
+ ]);
+ throw new Forbidden('AddressBook limit reached', 0);
+ }
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CardDAV/Sharing/Backend.php b/apps/dav/lib/CardDAV/Sharing/Backend.php
index f0f53ba9cfa..557115762fc 100644
--- a/apps/dav/lib/CardDAV/Sharing/Backend.php
+++ b/apps/dav/lib/CardDAV/Sharing/Backend.php
@@ -2,22 +2,8 @@
declare(strict_types=1);
/**
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Sharing;
@@ -30,7 +16,8 @@ use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class Backend extends SharingBackend {
- public function __construct(private IUserManager $userManager,
+ public function __construct(
+ private IUserManager $userManager,
private IGroupManager $groupManager,
private Principal $principalBackend,
private ICacheFactory $cacheFactory,
diff --git a/apps/dav/lib/CardDAV/Sharing/Service.php b/apps/dav/lib/CardDAV/Sharing/Service.php
index 5da71defb5e..1ab208f7ec3 100644
--- a/apps/dav/lib/CardDAV/Sharing/Service.php
+++ b/apps/dav/lib/CardDAV/Sharing/Service.php
@@ -2,22 +2,8 @@
declare(strict_types=1);
/**
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV\Sharing;
@@ -27,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService;
class Service extends SharingService {
protected string $resourceType = 'addressbook';
- public function __construct(protected SharingMapper $mapper) {
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
parent::__construct($mapper);
}
}
diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php
index 01747a9b105..e6da3ed5923 100644
--- a/apps/dav/lib/CardDAV/SyncService.php
+++ b/apps/dav/lib/CardDAV/SyncService.php
@@ -1,77 +1,53 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Http;
+use OCP\DB\Exception;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
+use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
-use Sabre\DAV\Client;
use Sabre\DAV\Xml\Response\MultiStatus;
use Sabre\DAV\Xml\Service;
-use Sabre\HTTP\ClientHttpException;
use Sabre\VObject\Reader;
+use Sabre\Xml\ParseException;
use function is_null;
class SyncService {
use TTransactional;
-
- private CardDavBackend $backend;
- private IUserManager $userManager;
- private IDBConnection $dbConnection;
- private LoggerInterface $logger;
private ?array $localSystemAddressBook = null;
- private Converter $converter;
protected string $certPath;
- public function __construct(CardDavBackend $backend,
- IUserManager $userManager,
- IDBConnection $dbConnection,
- LoggerInterface $logger,
- Converter $converter) {
- $this->backend = $backend;
- $this->userManager = $userManager;
- $this->logger = $logger;
- $this->converter = $converter;
+ public function __construct(
+ private CardDavBackend $backend,
+ private IUserManager $userManager,
+ private IDBConnection $dbConnection,
+ private LoggerInterface $logger,
+ private Converter $converter,
+ private IClientService $clientService,
+ private IConfig $config,
+ ) {
$this->certPath = '';
- $this->dbConnection = $dbConnection;
}
/**
+ * @psalm-return list{0: ?string, 1: boolean}
* @throws \Exception
*/
- public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
+ public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): array {
// 1. create addressbook
$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
$addressBookId = $book['id'];
@@ -79,7 +55,7 @@ class SyncService {
// 2. query changes
try {
$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
- } catch (ClientHttpException $ex) {
+ } catch (ClientExceptionInterface $ex) {
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
// remote server revoked access to the address book, remove it
$this->backend->deleteAddressBook($addressBookId);
@@ -96,12 +72,12 @@ class SyncService {
$cardUri = basename($resource);
if (isset($status[200])) {
$vCard = $this->download($url, $userName, $sharedSecret, $resource);
- $this->atomic(function () use ($addressBookId, $cardUri, $vCard) {
+ $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
$existingCard = $this->backend->getCard($addressBookId, $cardUri);
if ($existingCard === false) {
- $this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
+ $this->backend->createCard($addressBookId, $cardUri, $vCard);
} else {
- $this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
+ $this->backend->updateCard($addressBookId, $cardUri, $vCard);
}
}, $this->dbConnection);
} else {
@@ -109,82 +85,134 @@ class SyncService {
}
}
- return $response['token'];
+ return [
+ $response['token'],
+ $response['truncated'],
+ ];
}
/**
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
- return $this->atomic(function () use ($principal, $uri, $properties) {
- $book = $this->backend->getAddressBooksByUri($principal, $uri);
- if (!is_null($book)) {
- return $book;
- }
- $this->backend->createAddressBook($principal, $uri, $properties);
-
- return $this->backend->getAddressBooksByUri($principal, $uri);
- }, $this->dbConnection);
- }
+ try {
+ return $this->atomic(function () use ($principal, $uri, $properties) {
+ $book = $this->backend->getAddressBooksByUri($principal, $uri);
+ if (!is_null($book)) {
+ return $book;
+ }
+ $this->backend->createAddressBook($principal, $uri, $properties);
- /**
- * Check if there is a valid certPath we should use
- */
- protected function getCertPath(): string {
+ return $this->backend->getAddressBooksByUri($principal, $uri);
+ }, $this->dbConnection);
+ } catch (Exception $e) {
+ // READ COMMITTED doesn't prevent a nonrepeatable read above, so
+ // two processes might create an address book here. Ignore our
+ // failure and continue loading the entry written by the other process
+ if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ throw $e;
+ }
- // we already have a valid certPath
- if ($this->certPath !== '') {
- return $this->certPath;
+ // If this fails we might have hit a replication node that does not
+ // have the row written in the other process.
+ // TODO: find an elegant way to handle this
+ $ab = $this->backend->getAddressBooksByUri($principal, $uri);
+ if ($ab === null) {
+ throw new Exception('Could not create system address book', $e->getCode(), $e);
+ }
+ return $ab;
}
+ }
- $certManager = \OC::$server->getCertificateManager();
- $certPath = $certManager->getAbsoluteBundlePath();
- if (file_exists($certPath)) {
- $this->certPath = $certPath;
- }
+ public function ensureLocalSystemAddressBookExists(): ?array {
+ return $this->ensureSystemAddressBookExists('principals/system/system', 'system', [
+ '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
+ ]);
+ }
- return $this->certPath;
+ private function prepareUri(string $host, string $path): string {
+ /*
+ * The trailing slash is important for merging the uris.
+ *
+ * $host is stored in oc_trusted_servers.url and usually without a trailing slash.
+ *
+ * Example for a report request
+ *
+ * $host = 'https://server.internal/cloud'
+ * $path = 'remote.php/dav/addressbooks/system/system/system'
+ *
+ * Without the trailing slash, the webroot is missing:
+ * https://server.internal/remote.php/dav/addressbooks/system/system/system
+ *
+ * Example for a download request
+ *
+ * $host = 'https://server.internal/cloud'
+ * $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf'
+ *
+ * The response from the remote usually contains the webroot already and must be normalized to:
+ * https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
+ */
+ $host = rtrim($host, '/') . '/';
+
+ $uri = \GuzzleHttp\Psr7\UriResolver::resolve(
+ \GuzzleHttp\Psr7\Utils::uriFor($host),
+ \GuzzleHttp\Psr7\Utils::uriFor($path)
+ );
+
+ return (string)$uri;
}
- protected function getClient(string $url, string $userName, string $sharedSecret): Client {
- $settings = [
- 'baseUri' => $url . '/',
- 'userName' => $userName,
- 'password' => $sharedSecret,
+ /**
+ * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
+ * @throws ClientExceptionInterface
+ * @throws ParseException
+ */
+ protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
+ $client = $this->clientService->newClient();
+ $uri = $this->prepareUri($url, $addressBookUrl);
+
+ $options = [
+ 'auth' => [$userName, $sharedSecret],
+ 'body' => $this->buildSyncCollectionRequestBody($syncToken),
+ 'headers' => ['Content-Type' => 'application/xml'],
+ 'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT),
+ 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
];
- $client = new Client($settings);
- $certPath = $this->getCertPath();
- $client->setThrowExceptions(true);
- if ($certPath !== '' && !str_starts_with($url, 'http://')) {
- $client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
- }
+ $response = $client->request(
+ 'REPORT',
+ $uri,
+ $options
+ );
- return $client;
- }
+ $body = $response->getBody();
+ assert(is_string($body));
- protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
- $client = $this->getClient($url, $userName, $sharedSecret);
+ return $this->parseMultiStatus($body, $addressBookUrl);
+ }
- $body = $this->buildSyncCollectionRequestBody($syncToken);
+ protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
+ $client = $this->clientService->newClient();
+ $uri = $this->prepareUri($url, $resourcePath);
- $response = $client->request('REPORT', $addressBookUrl, $body, [
- 'Content-Type' => 'application/xml'
- ]);
+ $options = [
+ 'auth' => [$userName, $sharedSecret],
+ 'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
+ ];
- return $this->parseMultiStatus($response['body']);
- }
+ $response = $client->get(
+ $uri,
+ $options
+ );
- protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): array {
- $client = $this->getClient($url, $userName, $sharedSecret);
- return $client->request('GET', $resourcePath);
+ return (string)$response->getBody();
}
private function buildSyncCollectionRequestBody(?string $syncToken): string {
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElementNS('DAV:', 'd:sync-collection');
- $sync = $dom->createElement('d:sync-token', $syncToken);
+ $sync = $dom->createElement('d:sync-token', $syncToken ?? '');
$prop = $dom->createElement('d:prop');
$cont = $dom->createElement('d:getcontenttype');
$etag = $dom->createElement('d:getetag');
@@ -198,22 +226,50 @@ class SyncService {
}
/**
- * @param string $body
- * @return array
- * @throws \Sabre\Xml\ParseException
+ * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
+ * @throws ParseException
*/
- private function parseMultiStatus($body) {
- $xml = new Service();
-
+ private function parseMultiStatus(string $body, string $addressBookUrl): array {
/** @var MultiStatus $multiStatus */
- $multiStatus = $xml->expect('{DAV:}multistatus', $body);
+ $multiStatus = (new Service())->expect('{DAV:}multistatus', $body);
$result = [];
+ $truncated = false;
+
foreach ($multiStatus->getResponses() as $response) {
- $result[$response->getHref()] = $response->getResponseProperties();
+ $href = $response->getHref();
+ if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) {
+ $truncated = true;
+ } else {
+ $result[$response->getHref()] = $response->getResponseProperties();
+ }
}
- return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
+ return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
+ }
+
+ /**
+ * Determines whether the provided response URI corresponds to the given request URI.
+ */
+ private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
+ /*
+ * Example response uri:
+ *
+ * /remote.php/dav/addressbooks/system/system/system/
+ * /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
+ *
+ * Example request uri:
+ *
+ * remote.php/dav/addressbooks/system/system/system
+ *
+ * References:
+ * https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
+ * https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
+ */
+ return str_ends_with(
+ rtrim($responseUri, '/'),
+ rtrim($requestUri, '/')
+ );
}
/**
@@ -225,7 +281,7 @@ class SyncService {
$cardId = self::getCardUri($user);
if ($user->isEnabled()) {
- $this->atomic(function () use ($addressBookId, $cardId, $user) {
+ $this->atomic(function () use ($addressBookId, $cardId, $user): void {
$card = $this->backend->getCard($addressBookId, $cardId);
if ($card === false) {
$vCard = $this->converter->createCardFromUser($user);
@@ -262,10 +318,7 @@ class SyncService {
*/
public function getLocalSystemAddressBook() {
if (is_null($this->localSystemAddressBook)) {
- $systemPrincipal = "principals/system/system";
- $this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
- '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
- ]);
+ $this->localSystemAddressBook = $this->ensureLocalSystemAddressBookExists();
}
return $this->localSystemAddressBook;
@@ -276,7 +329,7 @@ class SyncService {
*/
public function syncInstance(?\Closure $progressCallback = null) {
$systemAddressBook = $this->getLocalSystemAddressBook();
- $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) {
+ $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback): void {
$this->updateUser($user);
if (!is_null($progressCallback)) {
$progressCallback();
diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php
index dc5ee0e1f21..912a2f1dcee 100644
--- a/apps/dav/lib/CardDAV/SystemAddressbook.php
+++ b/apps/dav/lib/CardDAV/SystemAddressbook.php
@@ -3,32 +3,11 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CardDAV;
-use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use OCA\Federation\TrustedServers;
use OCP\Accounts\IAccountManager;
use OCP\IConfig;
@@ -50,27 +29,18 @@ use function in_array;
class SystemAddressbook extends AddressBook {
public const URI_SHARED = 'z-server-generated--system';
- /** @var IConfig */
- private $config;
- private IUserSession $userSession;
- private ?TrustedServers $trustedServers;
- private ?IRequest $request;
- private ?IGroupManager $groupManager;
- public function __construct(BackendInterface $carddavBackend,
+ public function __construct(
+ BackendInterface $carddavBackend,
array $addressBookInfo,
IL10N $l10n,
- IConfig $config,
- IUserSession $userSession,
- ?IRequest $request = null,
- ?TrustedServers $trustedServers = null,
- ?IGroupManager $groupManager = null) {
+ private IConfig $config,
+ private IUserSession $userSession,
+ private ?IRequest $request = null,
+ private ?TrustedServers $trustedServers = null,
+ private ?IGroupManager $groupManager = null,
+ ) {
parent::__construct($carddavBackend, $addressBookInfo, $l10n);
- $this->config = $config;
- $this->userSession = $userSession;
- $this->request = $request;
- $this->trustedServers = $trustedServers;
- $this->groupManager = $groupManager;
$this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts');
$this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts');
@@ -241,14 +211,7 @@ class SystemAddressbook extends AddressBook {
}
return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
}
-
- /**
- * @throws UnsupportedLimitOnInitialSyncException
- */
public function getChanges($syncToken, $syncLevel, $limit = null) {
- if (!$syncToken && $limit) {
- throw new UnsupportedLimitOnInitialSyncException();
- }
if (!$this->carddavBackend instanceof SyncSupport) {
return null;
@@ -274,7 +237,7 @@ class SystemAddressbook extends AddressBook {
try {
$this->getChild($uri);
$added[] = $uri;
- } catch (NotFound | Forbidden $e) {
+ } catch (NotFound|Forbidden $e) {
$deleted[] = $uri;
}
}
@@ -282,7 +245,7 @@ class SystemAddressbook extends AddressBook {
try {
$this->getChild($uri);
$modified[] = $uri;
- } catch (NotFound | Forbidden $e) {
+ } catch (NotFound|Forbidden $e) {
$deleted[] = $uri;
}
}
diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php
index 2d129410067..e29e52e77df 100644
--- a/apps/dav/lib/CardDAV/UserAddressBooks.php
+++ b/apps/dav/lib/CardDAV/UserAddressBooks.php
@@ -3,28 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV;
@@ -39,6 +20,7 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Sabre\CardDAV\Backend;
@@ -54,20 +36,14 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
/** @var IConfig */
protected $config;
- /** @var PluginManager */
- private $pluginManager;
- private ?IUser $user;
- private ?IGroupManager $groupManager;
-
- public function __construct(Backend\BackendInterface $carddavBackend,
+ public function __construct(
+ Backend\BackendInterface $carddavBackend,
string $principalUri,
- PluginManager $pluginManager,
- ?IUser $user,
- ?IGroupManager $groupManager) {
+ private PluginManager $pluginManager,
+ private ?IUser $user,
+ private ?IGroupManager $groupManager,
+ ) {
parent::__construct($carddavBackend, $principalUri);
- $this->pluginManager = $pluginManager;
- $this->user = $user;
- $this->groupManager = $groupManager;
}
/**
@@ -80,7 +56,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$this->l10n = \OC::$server->getL10N('dav');
}
if ($this->config === null) {
- $this->config = \OC::$server->getConfig();
+ $this->config = Server::get(IConfig::class);
}
/** @var string|array $principal */
@@ -106,9 +82,9 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$trustedServers = null;
$request = null;
try {
- $trustedServers = \OC::$server->get(TrustedServers::class);
- $request = \OC::$server->get(IRequest::class);
- } catch (QueryException | NotFoundExceptionInterface | ContainerExceptionInterface $e) {
+ $trustedServers = Server::get(TrustedServers::class);
+ $request = Server::get(IRequest::class);
+ } catch (QueryException|NotFoundExceptionInterface|ContainerExceptionInterface $e) {
// nothing to do, the request / trusted servers don't exist
}
if ($addressBook['principaluri'] === 'principals/system/system') {
@@ -117,7 +93,7 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome {
$addressBook,
$this->l10n,
$this->config,
- \OCP\Server::get(IUserSession::class),
+ Server::get(IUserSession::class),
$request,
$trustedServers,
$this->groupManager
diff --git a/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php
new file mode 100644
index 00000000000..a5fd80ec124
--- /dev/null
+++ b/apps/dav/lib/CardDAV/Validation/CardDavValidatePlugin.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CardDAV\Validation;
+
+use OCA\DAV\AppInfo\Application;
+use OCP\IAppConfig;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class CardDavValidatePlugin extends ServerPlugin {
+
+ public function __construct(
+ private IAppConfig $config,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $server->on('beforeMethod:PUT', [$this, 'beforePut']);
+ }
+
+ public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
+ // evaluate if card size exceeds defined limit
+ $cardSizeLimit = $this->config->getValueInt(Application::APP_ID, 'card_size_limit', 5242880);
+ if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $cardSizeLimit) {
+ throw new Forbidden("VCard object exceeds $cardSizeLimit bytes");
+ }
+ // all tests passed return true
+ return true;
+ }
+
+}
diff --git a/apps/dav/lib/CardDAV/Xml/Groups.php b/apps/dav/lib/CardDAV/Xml/Groups.php
index bde36129382..07aeecb3fa2 100644
--- a/apps/dav/lib/CardDAV/Xml/Groups.php
+++ b/apps/dav/lib/CardDAV/Xml/Groups.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CardDAV\Xml;
@@ -29,14 +13,12 @@ use Sabre\Xml\XmlSerializable;
class Groups implements XmlSerializable {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
- /** @var string[] of TYPE:CHECKSUM */
- private $groups;
-
/**
- * @param string $groups
+ * @param list<string> $groups
*/
- public function __construct($groups) {
- $this->groups = $groups;
+ public function __construct(
+ private array $groups,
+ ) {
}
public function xmlSerialize(Writer $writer) {
diff --git a/apps/dav/lib/Command/ClearCalendarUnshares.php b/apps/dav/lib/Command/ClearCalendarUnshares.php
new file mode 100644
index 00000000000..bb367a9cd0f
--- /dev/null
+++ b/apps/dav/lib/Command/ClearCalendarUnshares.php
@@ -0,0 +1,114 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Sharing\Backend;
+use OCA\DAV\CalDAV\Sharing\Service;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend as BackendAlias;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IAppConfig;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+#[AsCommand(
+ name: 'dav:clear-calendar-unshares',
+ description: 'Clear calendar unshares for a user',
+ hidden: false,
+)]
+class ClearCalendarUnshares extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private IAppConfig $appConfig,
+ private Principal $principal,
+ private CalDavBackend $caldav,
+ private Backend $sharingBackend,
+ private Service $sharingService,
+ private SharingMapper $mapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User whose unshares to clear'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $principal = $this->principal->getPrincipalByPath('principals/users/' . $user);
+ if ($principal === null) {
+ throw new \InvalidArgumentException("Unable to fetch principal for user $user ");
+ }
+
+ $shares = $this->mapper->getSharesByPrincipals([$principal['uri']], 'calendar');
+ $unshares = array_filter($shares, static fn ($share) => $share['access'] === BackendAlias::ACCESS_UNSHARED);
+
+ if (count($unshares) === 0) {
+ $output->writeln("User $user has no calendar unshares");
+ return self::SUCCESS;
+ }
+
+ $rows = array_map(fn ($share) => $this->formatCalendarUnshare($share), $shares);
+
+ $table = new Table($output);
+ $table
+ ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name'])
+ ->setRows($rows)
+ ->render();
+
+ $output->writeln('');
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Please confirm to delete the above calendar unshare entries [y/n]', false);
+
+ if ($helper->ask($input, $output, $question)) {
+ $this->mapper->deleteUnsharesByPrincipal($principal['uri'], 'calendar');
+ $output->writeln("Calendar unshares for user $user deleted");
+ }
+
+ return self::SUCCESS;
+ }
+
+ private function formatCalendarUnshare(array $share): array {
+ $calendarInfo = $this->caldav->getCalendarById($share['resourceid']);
+
+ $resourceUri = 'Resource not found';
+ $resourceName = '';
+
+ if ($calendarInfo !== null) {
+ $resourceUri = $calendarInfo['uri'];
+ $resourceName = $calendarInfo['{DAV:}displayname'];
+ }
+
+ return [
+ $share['id'],
+ $share['resourceid'],
+ $resourceUri,
+ $resourceName,
+ ];
+ }
+}
diff --git a/apps/dav/lib/Command/ClearContactsPhotoCache.php b/apps/dav/lib/Command/ClearContactsPhotoCache.php
new file mode 100644
index 00000000000..82e64c3145a
--- /dev/null
+++ b/apps/dav/lib/Command/ClearContactsPhotoCache.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\NotPermittedException;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+
+#[AsCommand(
+ name: 'dav:clear-contacts-photo-cache',
+ description: 'Clear cached contact photos',
+ hidden: false,
+)]
+class ClearContactsPhotoCache extends Command {
+
+ public function __construct(
+ private IAppDataFactory $appDataFactory,
+ ) {
+ parent::__construct();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $photoCacheAppData = $this->appDataFactory->get('dav-photocache');
+
+ $folders = $photoCacheAppData->getDirectoryListing();
+ $countFolders = count($folders);
+
+ if ($countFolders === 0) {
+ $output->writeln('No cached contact photos found.');
+ return self::SUCCESS;
+ }
+
+ $output->writeln('Found ' . count($folders) . ' cached contact photos.');
+
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Please confirm to clear the contacts photo cache [y/n] ', true);
+
+ if ($helper->ask($input, $output, $question) === false) {
+ $output->writeln('Clearing the contacts photo cache aborted.');
+ return self::SUCCESS;
+ }
+
+ $progressBar = new ProgressBar($output, $countFolders);
+ $progressBar->start();
+
+ foreach ($folders as $folder) {
+ try {
+ $folder->delete();
+ } catch (NotPermittedException) {
+ }
+ $progressBar->advance();
+ }
+
+ $progressBar->finish();
+
+ $output->writeln('');
+ $output->writeln('Contacts photo cache cleared.');
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/CreateAddressBook.php b/apps/dav/lib/Command/CreateAddressBook.php
index 27ecb5973e4..9626edeba26 100644
--- a/apps/dav/lib/Command/CreateAddressBook.php
+++ b/apps/dav/lib/Command/CreateAddressBook.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Command;
@@ -41,14 +24,14 @@ class CreateAddressBook extends Command {
protected function configure(): void {
$this
- ->setName('dav:create-addressbook')
- ->setDescription('Create a dav addressbook')
- ->addArgument('user',
- InputArgument::REQUIRED,
- 'User for whom the addressbook will be created')
- ->addArgument('name',
- InputArgument::REQUIRED,
- 'Name of the addressbook');
+ ->setName('dav:create-addressbook')
+ ->setDescription('Create a dav addressbook')
+ ->addArgument('user',
+ InputArgument::REQUIRED,
+ 'User for whom the addressbook will be created')
+ ->addArgument('name',
+ InputArgument::REQUIRED,
+ 'Name of the addressbook');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php
index 9acc1e147f8..033b5f8d347 100644
--- a/apps/dav/lib/Command/CreateCalendar.php
+++ b/apps/dav/lib/Command/CreateCalendar.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Command;
@@ -31,11 +13,15 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\Connector\Sabre\Principal;
use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -71,19 +57,19 @@ class CreateCalendar extends Command {
$principalBackend = new Principal(
$this->userManager,
$this->groupManager,
- \OC::$server->get(IAccountManager::class),
- \OC::$server->getShareManager(),
- \OC::$server->getUserSession(),
- \OC::$server->getAppManager(),
- \OC::$server->query(ProxyMapper::class),
- \OC::$server->get(KnownUserService::class),
- \OC::$server->getConfig(),
+ Server::get(IAccountManager::class),
+ Server::get(\OCP\Share\IManager::class),
+ Server::get(IUserSession::class),
+ Server::get(IAppManager::class),
+ Server::get(ProxyMapper::class),
+ Server::get(KnownUserService::class),
+ Server::get(IConfig::class),
\OC::$server->getL10NFactory(),
);
- $random = \OC::$server->getSecureRandom();
- $logger = \OC::$server->get(LoggerInterface::class);
- $dispatcher = \OC::$server->get(IEventDispatcher::class);
- $config = \OC::$server->get(IConfig::class);
+ $random = Server::get(ISecureRandom::class);
+ $logger = Server::get(LoggerInterface::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
+ $config = Server::get(IConfig::class);
$name = $input->getArgument('name');
$caldav = new CalDavBackend(
$this->dbConnection,
@@ -93,7 +79,7 @@ class CreateCalendar extends Command {
$logger,
$dispatcher,
$config,
- \OC::$server->get(Backend::class),
+ Server::get(Backend::class),
);
$caldav->createCalendar("principals/users/$user", $name, []);
return self::SUCCESS;
diff --git a/apps/dav/lib/Command/CreateSubscription.php b/apps/dav/lib/Command/CreateSubscription.php
new file mode 100644
index 00000000000..1364070e530
--- /dev/null
+++ b/apps/dav/lib/Command/CreateSubscription.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\Theming\ThemingDefaults;
+use OCP\IUserManager;
+use Sabre\DAV\Xml\Property\Href;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class CreateSubscription extends Command {
+ public function __construct(
+ protected IUserManager $userManager,
+ private CalDavBackend $caldav,
+ private ThemingDefaults $themingDefaults,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('dav:create-subscription')
+ ->setDescription('Create a dav subscription')
+ ->addArgument('user',
+ InputArgument::REQUIRED,
+ 'User for whom the subscription will be created')
+ ->addArgument('name',
+ InputArgument::REQUIRED,
+ 'Name of the subscription to create')
+ ->addArgument('url',
+ InputArgument::REQUIRED,
+ 'Source url of the subscription to create')
+ ->addArgument('color',
+ InputArgument::OPTIONAL,
+ 'Hex color code for the calendar color');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $input->getArgument('user');
+ if (!$this->userManager->userExists($user)) {
+ $output->writeln("<error>User <$user> in unknown.</error>");
+ return self::FAILURE;
+ }
+
+ $name = $input->getArgument('name');
+ $url = $input->getArgument('url');
+ $color = $input->getArgument('color') ?? $this->themingDefaults->getColorPrimary();
+ $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user");
+
+ $exists = array_filter($subscriptions, function ($row) use ($url) {
+ return $row['source'] === $url;
+ });
+
+ if (!empty($exists)) {
+ $output->writeln("<error>Subscription for url <$url> already exists for this user.</error>");
+ return self::FAILURE;
+ }
+
+ $urlProperty = new Href($url);
+ $properties = ['{http://owncloud.org/ns}calendar-enabled' => 1,
+ '{DAV:}displayname' => $name,
+ '{http://apple.com/ns/ical/}calendar-color' => $color,
+ '{http://calendarserver.org/ns/}source' => $urlProperty,
+ ];
+ $this->caldav->createSubscription("principals/users/$user", $name, $properties);
+ return self::SUCCESS;
+ }
+
+}
diff --git a/apps/dav/lib/Command/DeleteCalendar.php b/apps/dav/lib/Command/DeleteCalendar.php
index f9caa142757..f6dbed856e6 100644
--- a/apps/dav/lib/Command/DeleteCalendar.php
+++ b/apps/dav/lib/Command/DeleteCalendar.php
@@ -2,24 +2,8 @@
declare(strict_types=1);
/**
- *
- * @copyright Copyright (c) 2021, Mattia Narducci (mattianarducci1@gmail.com)
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
@@ -70,9 +54,9 @@ class DeleteCalendar extends Command {
protected function execute(
InputInterface $input,
- OutputInterface $output
+ OutputInterface $output,
): int {
- /** @var string $user **/
+ /** @var string $user */
$user = $input->getArgument('uid');
if (!$this->userManager->userExists($user)) {
throw new \InvalidArgumentException(
@@ -83,7 +67,7 @@ class DeleteCalendar extends Command {
if ($birthday !== false) {
$name = BirthdayService::BIRTHDAY_CALENDAR_URI;
} else {
- /** @var string $name **/
+ /** @var string $name */
$name = $input->getArgument('name');
if (!$name) {
throw new \InvalidArgumentException(
diff --git a/apps/dav/lib/Command/DeleteSubscription.php b/apps/dav/lib/Command/DeleteSubscription.php
new file mode 100644
index 00000000000..db0cb6295c9
--- /dev/null
+++ b/apps/dav/lib/Command/DeleteSubscription.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CachedSubscription;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:delete-subscription',
+ description: 'Delete a calendar subscription for a user',
+ hidden: false,
+)]
+class DeleteSubscription extends Command {
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private IUserManager $userManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User who owns the calendar subscription'
+ )
+ ->addArgument(
+ 'uri',
+ InputArgument::REQUIRED,
+ 'URI of the calendar to be deleted'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $uri = (string)$input->getArgument('uri');
+ if ($uri === '') {
+ throw new \InvalidArgumentException('Specify the URI of the calendar to be deleted');
+ }
+
+ $subscriptionInfo = $this->calDavBackend->getSubscriptionByUri(
+ 'principals/users/' . $user,
+ $uri
+ );
+
+ if ($subscriptionInfo === null) {
+ throw new \InvalidArgumentException("User $user has no calendar subscription with the URI $uri");
+ }
+
+ $subscription = new CachedSubscription(
+ $this->calDavBackend,
+ $subscriptionInfo,
+ );
+
+ $subscription->delete();
+
+ $output->writeln("Calendar subscription with the URI $uri for user $user deleted");
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php
new file mode 100644
index 00000000000..6ed8aa2cfe4
--- /dev/null
+++ b/apps/dav/lib/Command/ExportCalendar.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Command;
+
+use InvalidArgumentException;
+use OCA\DAV\CalDAV\Export\ExportService;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\Calendar\IManager;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Calendar Export Command
+ *
+ * Used to export data from supported calendars to disk or stdout
+ */
+#[AsCommand(
+ name: 'calendar:export',
+ description: 'Export calendar data from supported calendars to disk or stdout',
+ hidden: false
+)]
+class ExportCalendar extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private IManager $calendarManager,
+ private ExportService $exportService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('calendar:export')
+ ->setDescription('Export calendar data from supported calendars to disk or stdout')
+ ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
+ ->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar')
+ ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical')
+ ->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the output. defaults to stdout');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('uid');
+ $calendarId = $input->getArgument('uri');
+ $format = $input->getOption('format');
+ $location = $input->getOption('location');
+
+ if (!$this->userManager->userExists($userId)) {
+ throw new InvalidArgumentException("User <$userId> not found.");
+ }
+ // retrieve calendar and evaluate if export is supported
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ throw new InvalidArgumentException("Calendar <$calendarId> not found.");
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarExport) {
+ throw new InvalidArgumentException("Calendar <$calendarId> does not support exporting");
+ }
+ // construct options object
+ $options = new CalendarExportOptions();
+ // evaluate if provided format is supported
+ if (!in_array($format, ExportService::FORMATS, true)) {
+ throw new InvalidArgumentException("Format <$format> is not valid.");
+ }
+ $options->setFormat($format);
+ // evaluate is a valid location was given and is usable otherwise output to stdout
+ if ($location !== null) {
+ $handle = fopen($location, 'wb');
+ if ($handle === false) {
+ throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation.");
+ }
+
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ fwrite($handle, $chunk);
+ }
+ fclose($handle);
+ } else {
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ $output->writeln($chunk);
+ }
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/FixCalendarSyncCommand.php b/apps/dav/lib/Command/FixCalendarSyncCommand.php
index 6db32ff6d5e..cb31355c10d 100644
--- a/apps/dav/lib/Command/FixCalendarSyncCommand.php
+++ b/apps/dav/lib/Command/FixCalendarSyncCommand.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
@@ -36,8 +20,10 @@ use Symfony\Component\Console\Output\OutputInterface;
class FixCalendarSyncCommand extends Command {
- public function __construct(private IUserManager $userManager,
- private CalDavBackend $calDavBackend) {
+ public function __construct(
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ ) {
parent::__construct('dav:fix-missing-caldav-changes');
}
@@ -57,22 +43,23 @@ class FixCalendarSyncCommand extends Command {
$user = $this->userManager->get($userArg);
if ($user === null) {
$output->writeln("<error>User $userArg does not exist</error>");
- return 1;
+ return self::FAILURE;
}
$this->fixUserCalendars($user);
} else {
$progress = new ProgressBar($output);
- $this->userManager->callForSeenUsers(function (IUser $user) use ($progress) {
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($progress): void {
$this->fixUserCalendars($user, $progress);
});
$progress->finish();
}
- return 0;
+ $output->writeln('');
+ return self::SUCCESS;
}
private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void {
- $calendars = $this->calDavBackend->getCalendarsForUser("principals/users/" . $user->getUID());
+ $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $user->getUID());
foreach ($calendars as $calendar) {
$this->calDavBackend->restoreChanges($calendar['id']);
diff --git a/apps/dav/lib/Command/GetAbsenceCommand.php b/apps/dav/lib/Command/GetAbsenceCommand.php
new file mode 100644
index 00000000000..50d8df4ab38
--- /dev/null
+++ b/apps/dav/lib/Command/GetAbsenceCommand.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\Service\AbsenceService;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class GetAbsenceCommand extends Command {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private AbsenceService $absenceService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('dav:absence:get');
+ $this->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'User ID of the affected account'
+ );
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user-id');
+
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln('<error>User not found</error>');
+ return 1;
+ }
+
+ $absence = $this->absenceService->getAbsence($userId);
+ if ($absence === null) {
+ $output->writeln('<info>No absence set</info>');
+ return 0;
+ }
+
+ $output->writeln('<info>Start day:</info> ' . $absence->getFirstDay());
+ $output->writeln('<info>End day:</info> ' . $absence->getLastDay());
+ $output->writeln('<info>Short message:</info> ' . $absence->getStatus());
+ $output->writeln('<info>Message:</info> ' . $absence->getMessage());
+ $output->writeln('<info>Replacement user:</info> ' . ($absence->getReplacementUserId() ?? 'none'));
+ $output->writeln('<info>Replacement display name:</info> ' . ($absence->getReplacementUserDisplayName() ?? 'none'));
+
+ return 0;
+ }
+
+}
diff --git a/apps/dav/lib/Command/ListAddressbooks.php b/apps/dav/lib/Command/ListAddressbooks.php
new file mode 100644
index 00000000000..c0b6e63ccb8
--- /dev/null
+++ b/apps/dav/lib/Command/ListAddressbooks.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\SystemAddressbook;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListAddressbooks extends Command {
+ public function __construct(
+ protected IUserManager $userManager,
+ private CardDavBackend $cardDavBackend,
+ ) {
+ parent::__construct('dav:list-addressbooks');
+ }
+
+ protected function configure(): void {
+ $this
+ ->setDescription('List all addressbooks of a user')
+ ->addArgument('uid',
+ InputArgument::REQUIRED,
+ 'User for whom all addressbooks will be listed');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User <$user> is unknown.");
+ }
+
+ $addressBooks = $this->cardDavBackend->getAddressBooksForUser("principals/users/$user");
+
+ $addressBookTableData = [];
+ foreach ($addressBooks as $book) {
+ // skip system / contacts integration address book
+ if ($book['uri'] === SystemAddressbook::URI_SHARED) {
+ continue;
+ }
+
+ $readOnly = false;
+ $readOnlyIndex = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
+ if (isset($book[$readOnlyIndex])) {
+ $readOnly = $book[$readOnlyIndex];
+ }
+
+ $addressBookTableData[] = [
+ $book['uri'],
+ $book['{DAV:}displayname'],
+ $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] ?? $book['principaluri'],
+ $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'],
+ $readOnly ? ' x ' : ' ✓ ',
+ ];
+ }
+
+ if (count($addressBookTableData) > 0) {
+ $table = new Table($output);
+ $table->setHeaders(['Database ID', 'URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable'])
+ ->setRows($addressBookTableData);
+
+ $table->render();
+ } else {
+ $output->writeln("<info>User <$user> has no addressbooks</info>");
+ }
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/ListCalendarShares.php b/apps/dav/lib/Command/ListCalendarShares.php
new file mode 100644
index 00000000000..2729bc80530
--- /dev/null
+++ b/apps/dav/lib/Command/ListCalendarShares.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\Sharing\Backend;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:list-calendar-shares',
+ description: 'List all calendar shares for a user',
+ hidden: false,
+)]
+class ListCalendarShares extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private Principal $principal,
+ private CalDavBackend $caldav,
+ private SharingMapper $mapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User whose calendar shares will be listed'
+ );
+ $this->addOption(
+ 'calendar-id',
+ '',
+ InputOption::VALUE_REQUIRED,
+ 'List only shares for the given calendar id id',
+ null,
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $principal = $this->principal->getPrincipalByPath('principals/users/' . $user);
+ if ($principal === null) {
+ throw new \InvalidArgumentException("Unable to fetch principal for user $user");
+ }
+
+ $memberships = array_merge(
+ [$principal['uri']],
+ $this->principal->getGroupMembership($principal['uri']),
+ $this->principal->getCircleMembership($principal['uri']),
+ );
+
+ $shares = $this->mapper->getSharesByPrincipals($memberships, 'calendar');
+
+ $calendarId = $input->getOption('calendar-id');
+ if ($calendarId !== null) {
+ $shares = array_filter($shares, fn ($share) => $share['resourceid'] === (int)$calendarId);
+ }
+
+ $rows = array_map(fn ($share) => $this->formatCalendarShare($share), $shares);
+
+ if (count($rows) > 0) {
+ $table = new Table($output);
+ $table
+ ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name', 'Calendar Owner', 'Access By', 'Permissions'])
+ ->setRows($rows)
+ ->render();
+ } else {
+ $output->writeln("User $user has no calendar shares");
+ }
+
+ return self::SUCCESS;
+ }
+
+ private function formatCalendarShare(array $share): array {
+ $calendarInfo = $this->caldav->getCalendarById($share['resourceid']);
+
+ $calendarUri = 'Resource not found';
+ $calendarName = '';
+ $calendarOwner = '';
+
+ if ($calendarInfo !== null) {
+ $calendarUri = $calendarInfo['uri'];
+ $calendarName = $calendarInfo['{DAV:}displayname'];
+ $calendarOwner = $calendarInfo['{http://nextcloud.com/ns}owner-displayname'] . ' (' . $calendarInfo['principaluri'] . ')';
+ }
+
+ $accessBy = match (true) {
+ str_starts_with($share['principaluri'], 'principals/users/') => 'Individual',
+ str_starts_with($share['principaluri'], 'principals/groups/') => 'Group (' . $share['principaluri'] . ')',
+ str_starts_with($share['principaluri'], 'principals/circles/') => 'Team (' . $share['principaluri'] . ')',
+ default => $share['principaluri'],
+ };
+
+ $permissions = match ($share['access']) {
+ Backend::ACCESS_READ => 'Read',
+ Backend::ACCESS_READ_WRITE => 'Read/Write',
+ Backend::ACCESS_UNSHARED => 'Unshare',
+ default => $share['access'],
+ };
+
+ return [
+ $share['id'],
+ $share['resourceid'],
+ $calendarUri,
+ $calendarName,
+ $calendarOwner,
+ $accessBy,
+ $permissions,
+ ];
+ }
+}
diff --git a/apps/dav/lib/Command/ListCalendars.php b/apps/dav/lib/Command/ListCalendars.php
index 901d2822656..408a7e5247f 100644
--- a/apps/dav/lib/Command/ListCalendars.php
+++ b/apps/dav/lib/Command/ListCalendars.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
@@ -83,7 +64,7 @@ class ListCalendars extends Command {
if (count($calendarTableData) > 0) {
$table = new Table($output);
- $table->setHeaders(['uri', 'displayname', 'owner\'s userid', 'owner\'s displayname', 'writable'])
+ $table->setHeaders(['URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable'])
->setRows($calendarTableData);
$table->render();
diff --git a/apps/dav/lib/Command/ListSubscriptions.php b/apps/dav/lib/Command/ListSubscriptions.php
new file mode 100644
index 00000000000..67753f25973
--- /dev/null
+++ b/apps/dav/lib/Command/ListSubscriptions.php
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\IAppConfig;
+use OCP\IUserManager;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+ name: 'dav:list-subscriptions',
+ description: 'List all calendar subscriptions for a user',
+ hidden: false,
+)]
+class ListSubscriptions extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private IAppConfig $appConfig,
+ private CalDavBackend $caldav,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'User whose calendar subscriptions will be listed'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getArgument('uid');
+ if (!$this->userManager->userExists($user)) {
+ throw new \InvalidArgumentException("User $user is unknown");
+ }
+
+ $defaultRefreshRate = $this->appConfig->getValueString('dav', 'calendarSubscriptionRefreshRate', 'P1D');
+ $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user");
+ $rows = [];
+
+ foreach ($subscriptions as $subscription) {
+ $rows[] = [
+ $subscription['uri'],
+ $subscription['{DAV:}displayname'],
+ $subscription['{http://apple.com/ns/ical/}refreshrate'] ?? ($defaultRefreshRate . ' (default)'),
+ $subscription['source'],
+ ];
+ }
+
+ usort($rows, static fn (array $a, array $b) => $a[0] <=> $b[0]);
+
+ if (count($rows) > 0) {
+ $table = new Table($output);
+ $table
+ ->setHeaders(['URI', 'Displayname', 'Refresh rate', 'Source'])
+ ->setRows($rows)
+ ->render();
+ } else {
+ $output->writeln("User $user has no subscriptions");
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php
index 7269a5ad4f3..b8acc191cc3 100644
--- a/apps/dav/lib/Command/MoveCalendar.php
+++ b/apps/dav/lib/Command/MoveCalendar.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
@@ -71,7 +51,7 @@ class MoveCalendar extends Command {
->addArgument('destinationuid',
InputArgument::REQUIRED,
'User who will receive the calendar')
- ->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts");
+ ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the migration by removing existing shares and renaming calendars in case of conflicts');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
@@ -118,8 +98,8 @@ class MoveCalendar extends Command {
* Warn that share links have changed if there are shares
*/
$this->io->note([
- "Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.",
- "Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\""
+ 'Please note that moving calendar ' . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.",
+ 'Sharees will need to change "example.com/remote.php/dav/calendars/uid/' . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\""
]);
}
@@ -176,7 +156,7 @@ class MoveCalendar extends Command {
if ($force) {
$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/groups/' . $userOrGroup]);
} else {
- throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share.");
+ throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . '> was shared. You may use -f to move the calendar while deleting this share.');
}
}
@@ -187,7 +167,7 @@ class MoveCalendar extends Command {
if ($force) {
$this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/users/' . $userOrGroup]);
} else {
- throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share.");
+ throw new \InvalidArgumentException('The calendar <' . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share.");
}
}
}
diff --git a/apps/dav/lib/Command/RemoveInvalidShares.php b/apps/dav/lib/Command/RemoveInvalidShares.php
index e373dc1c678..340e878a912 100644
--- a/apps/dav/lib/Command/RemoveInvalidShares.php
+++ b/apps/dav/lib/Command/RemoveInvalidShares.php
@@ -2,27 +2,11 @@
declare(strict_types=1);
+
/**
- * @copyright Copyright (c) 2018, ownCloud GmbH
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2018 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Command;
@@ -54,7 +38,7 @@ class RemoveInvalidShares extends Command {
$query = $this->connection->getQueryBuilder();
$result = $query->selectDistinct('principaluri')
->from('dav_shares')
- ->execute();
+ ->executeQuery();
while ($row = $result->fetch()) {
$principaluri = $row['principaluri'];
@@ -75,6 +59,6 @@ class RemoveInvalidShares extends Command {
$delete = $this->connection->getQueryBuilder();
$delete->delete('dav_shares')
->where($delete->expr()->eq('principaluri', $delete->createNamedParameter($principaluri)));
- $delete->execute();
+ $delete->executeStatement();
}
}
diff --git a/apps/dav/lib/Command/RetentionCleanupCommand.php b/apps/dav/lib/Command/RetentionCleanupCommand.php
index 21eb96c235c..f1c941af20e 100644
--- a/apps/dav/lib/Command/RetentionCleanupCommand.php
+++ b/apps/dav/lib/Command/RetentionCleanupCommand.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php
index 16e3bb65a46..89bb5ce8c20 100644
--- a/apps/dav/lib/Command/SendEventReminders.php
+++ b/apps/dav/lib/Command/SendEventReminders.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
diff --git a/apps/dav/lib/Command/SetAbsenceCommand.php b/apps/dav/lib/Command/SetAbsenceCommand.php
new file mode 100644
index 00000000000..bf91a163f95
--- /dev/null
+++ b/apps/dav/lib/Command/SetAbsenceCommand.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Command;
+
+use OCA\DAV\Service\AbsenceService;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SetAbsenceCommand extends Command {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private AbsenceService $absenceService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this->setName('dav:absence:set');
+ $this->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'User ID of the affected account'
+ );
+ $this->addArgument(
+ 'first-day',
+ InputArgument::REQUIRED,
+ 'Inclusive start day formatted as YYYY-MM-DD'
+ );
+ $this->addArgument(
+ 'last-day',
+ InputArgument::REQUIRED,
+ 'Inclusive end day formatted as YYYY-MM-DD'
+ );
+ $this->addArgument(
+ 'short-message',
+ InputArgument::REQUIRED,
+ 'Short message'
+ );
+ $this->addArgument(
+ 'message',
+ InputArgument::REQUIRED,
+ 'Message'
+ );
+ $this->addArgument(
+ 'replacement-user-id',
+ InputArgument::OPTIONAL,
+ 'Replacement user id'
+ );
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user-id');
+
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln('<error>User not found</error>');
+ return 1;
+ }
+
+ $replacementUserId = $input->getArgument('replacement-user-id');
+ if ($replacementUserId === null) {
+ $replacementUser = null;
+ } else {
+ $replacementUser = $this->userManager->get($replacementUserId);
+ if ($replacementUser === null) {
+ $output->writeln('<error>Replacement user not found</error>');
+ return 2;
+ }
+ }
+
+ $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $input->getArgument('first-day'),
+ $input->getArgument('last-day'),
+ $input->getArgument('short-message'),
+ $input->getArgument('message'),
+ $replacementUser?->getUID(),
+ $replacementUser?->getDisplayName(),
+ );
+
+ return 0;
+ }
+
+}
diff --git a/apps/dav/lib/Command/SyncBirthdayCalendar.php b/apps/dav/lib/Command/SyncBirthdayCalendar.php
index 977fd5a067c..db1ebb6ecb5 100644
--- a/apps/dav/lib/Command/SyncBirthdayCalendar.php
+++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Command;
@@ -72,10 +55,10 @@ class SyncBirthdayCalendar extends Command {
$this->birthdayService->syncUser($user);
return self::SUCCESS;
}
- $output->writeln("Start birthday calendar sync for all users ...");
+ $output->writeln('Start birthday calendar sync for all users ...');
$p = new ProgressBar($output);
$p->start();
- $this->userManager->callForSeenUsers(function ($user) use ($p) {
+ $this->userManager->callForSeenUsers(function ($user) use ($p): void {
$p->advance();
$userId = $user->getUID();
diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php
index b05e65249ff..54edba01e05 100644
--- a/apps/dav/lib/Command/SyncSystemAddressBook.php
+++ b/apps/dav/lib/Command/SyncSystemAddressBook.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Command;
@@ -48,7 +32,7 @@ class SyncSystemAddressBook extends Command {
$output->writeln('Syncing users ...');
$progress = new ProgressBar($output);
$progress->start();
- $this->syncService->syncInstance(function () use ($progress) {
+ $this->syncService->syncInstance(function () use ($progress): void {
$progress->advance();
});
diff --git a/apps/dav/lib/Comments/CommentNode.php b/apps/dav/lib/Comments/CommentNode.php
index 1d43c9c9309..5dbefa82d93 100644
--- a/apps/dav/lib/Comments/CommentNode.php
+++ b/apps/dav/lib/Comments/CommentNode.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Comments;
@@ -46,37 +30,19 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
public const PROPERTY_NAME_MENTION_ID = '{http://owncloud.org/ns}mentionId';
public const PROPERTY_NAME_MENTION_DISPLAYNAME = '{http://owncloud.org/ns}mentionDisplayName';
- /** @var IComment */
- public $comment;
-
- /** @var ICommentsManager */
- protected $commentsManager;
-
- protected LoggerInterface $logger;
-
/** @var array list of properties with key being their name and value their setter */
protected $properties = [];
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IUserSession */
- protected $userSession;
-
/**
* CommentNode constructor.
*/
public function __construct(
- ICommentsManager $commentsManager,
- IComment $comment,
- IUserManager $userManager,
- IUserSession $userSession,
- LoggerInterface $logger
+ protected ICommentsManager $commentsManager,
+ public IComment $comment,
+ protected IUserManager $userManager,
+ protected IUserSession $userSession,
+ protected LoggerInterface $logger,
) {
- $this->commentsManager = $commentsManager;
- $this->comment = $comment;
- $this->logger = $logger;
-
$methods = get_class_methods($this->comment);
$methods = array_filter($methods, function ($name) {
return str_starts_with($name, 'get');
@@ -85,11 +51,9 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties {
if ($getter === 'getMentions') {
continue; // special treatment
}
- $name = '{'.self::NS_OWNCLOUD.'}' . lcfirst(substr($getter, 3));
+ $name = '{' . self::NS_OWNCLOUD . '}' . lcfirst(substr($getter, 3));
$this->properties[$name] = $getter;
}
- $this->userManager = $userManager;
- $this->userSession = $userSession;
}
/**
diff --git a/apps/dav/lib/Comments/CommentsPlugin.php b/apps/dav/lib/Comments/CommentsPlugin.php
index 1cfaa8b4e16..2ab7d6ee018 100644
--- a/apps/dav/lib/Comments/CommentsPlugin.php
+++ b/apps/dav/lib/Comments/CommentsPlugin.php
@@ -1,32 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Comments;
+use OCP\AppFramework\Http;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
+use OCP\Comments\MessageTooLongException;
use OCP\IUserSession;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\NotFound;
@@ -52,24 +36,19 @@ class CommentsPlugin extends ServerPlugin {
public const REPORT_PARAM_OFFSET = '{http://owncloud.org/ns}offset';
public const REPORT_PARAM_TIMESTAMP = '{http://owncloud.org/ns}datetime';
- /** @var ICommentsManager */
- protected $commentsManager;
-
/** @var \Sabre\DAV\Server $server */
private $server;
- /** @var \OCP\IUserSession */
- protected $userSession;
-
/**
* Comments plugin
*
* @param ICommentsManager $commentsManager
* @param IUserSession $userSession
*/
- public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) {
- $this->commentsManager = $commentsManager;
- $this->userSession = $userSession;
+ public function __construct(
+ protected ICommentsManager $commentsManager,
+ protected IUserSession $userSession,
+ ) {
}
/**
@@ -91,7 +70,7 @@ class CommentsPlugin extends ServerPlugin {
$this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
- $this->server->xml->classMap['DateTime'] = function (Writer $writer, \DateTime $value) {
+ $this->server->xml->classMap['DateTime'] = function (Writer $writer, \DateTime $value): void {
$writer->write(\Sabre\HTTP\toDate($value));
};
@@ -130,7 +109,7 @@ class CommentsPlugin extends ServerPlugin {
$response->setHeader('Content-Location', $url);
// created
- $response->setStatus(201);
+ $response->setStatus(Http::STATUS_CREATED);
return false;
}
@@ -199,7 +178,7 @@ class CommentsPlugin extends ServerPlugin {
new MultiStatus($responses)
);
- $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setBody($xml);
@@ -234,7 +213,7 @@ class CommentsPlugin extends ServerPlugin {
}
}
if (is_null($actorId)) {
- throw new BadRequest('Invalid actor "' . $actorType .'"');
+ throw new BadRequest('Invalid actor "' . $actorType . '"');
}
try {
@@ -245,9 +224,9 @@ class CommentsPlugin extends ServerPlugin {
return $comment;
} catch (\InvalidArgumentException $e) {
throw new BadRequest('Invalid input values', 0, $e);
- } catch (\OCP\Comments\MessageTooLongException $e) {
+ } catch (MessageTooLongException $e) {
$msg = 'Message exceeds allowed character limit of ';
- throw new BadRequest($msg . \OCP\Comments\IComment::MAX_MESSAGE_LENGTH, 0, $e);
+ throw new BadRequest($msg . IComment::MAX_MESSAGE_LENGTH, 0, $e);
}
}
}
diff --git a/apps/dav/lib/Comments/EntityCollection.php b/apps/dav/lib/Comments/EntityCollection.php
index 1b6139a34a2..33c58ee44d2 100644
--- a/apps/dav/lib/Comments/EntityCollection.php
+++ b/apps/dav/lib/Comments/EntityCollection.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Comments;
@@ -43,11 +27,6 @@ use Sabre\DAV\PropPatch;
class EntityCollection extends RootCollection implements IProperties {
public const PROPERTY_NAME_READ_MARKER = '{http://owncloud.org/ns}readMarker';
- /** @var string */
- protected $id;
-
- protected LoggerInterface $logger;
-
/**
* @param string $id
* @param string $name
@@ -57,12 +36,12 @@ class EntityCollection extends RootCollection implements IProperties {
* @param LoggerInterface $logger
*/
public function __construct(
- $id,
+ protected $id,
$name,
ICommentsManager $commentsManager,
IUserManager $userManager,
IUserSession $userSession,
- LoggerInterface $logger
+ protected LoggerInterface $logger,
) {
foreach (['id', 'name'] as $property) {
$$property = trim($$property);
@@ -70,10 +49,8 @@ class EntityCollection extends RootCollection implements IProperties {
throw new \InvalidArgumentException('"' . $property . '" parameter must be non-empty string');
}
}
- $this->id = $id;
$this->name = $name;
$this->commentsManager = $commentsManager;
- $this->logger = $logger;
$this->userManager = $userManager;
$this->userSession = $userSession;
}
diff --git a/apps/dav/lib/Comments/EntityTypeCollection.php b/apps/dav/lib/Comments/EntityTypeCollection.php
index 32adcee54f0..1c8533ca375 100644
--- a/apps/dav/lib/Comments/EntityTypeCollection.php
+++ b/apps/dav/lib/Comments/EntityTypeCollection.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Comments;
@@ -42,19 +26,13 @@ use Sabre\DAV\Exception\NotFound;
* @package OCA\DAV\Comments
*/
class EntityTypeCollection extends RootCollection {
- protected LoggerInterface $logger;
- protected IUserManager $userManager;
-
- /** @var \Closure */
- protected $childExistsFunction;
-
public function __construct(
string $name,
ICommentsManager $commentsManager,
- IUserManager $userManager,
+ protected IUserManager $userManager,
IUserSession $userSession,
- LoggerInterface $logger,
- \Closure $childExistsFunction
+ protected LoggerInterface $logger,
+ protected \Closure $childExistsFunction,
) {
$name = trim($name);
if (empty($name)) {
@@ -62,10 +40,7 @@ class EntityTypeCollection extends RootCollection {
}
$this->name = $name;
$this->commentsManager = $commentsManager;
- $this->logger = $logger;
- $this->userManager = $userManager;
$this->userSession = $userSession;
- $this->childExistsFunction = $childExistsFunction;
}
/**
diff --git a/apps/dav/lib/Comments/RootCollection.php b/apps/dav/lib/Comments/RootCollection.php
index 956980c900d..493d73ec531 100644
--- a/apps/dav/lib/Comments/RootCollection.php
+++ b/apps/dav/lib/Comments/RootCollection.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Comments;
@@ -38,24 +21,15 @@ use Sabre\DAV\ICollection;
class RootCollection implements ICollection {
/** @var EntityTypeCollection[]|null */
private ?array $entityTypeCollections = null;
- protected ICommentsManager $commentsManager;
protected string $name = 'comments';
- protected LoggerInterface $logger;
- protected IUserManager $userManager;
- protected IUserSession $userSession;
- protected IEventDispatcher $dispatcher;
public function __construct(
- ICommentsManager $commentsManager,
- IUserManager $userManager,
- IUserSession $userSession,
- IEventDispatcher $dispatcher,
- LoggerInterface $logger) {
- $this->commentsManager = $commentsManager;
- $this->logger = $logger;
- $this->userManager = $userManager;
- $this->userSession = $userSession;
- $this->dispatcher = $dispatcher;
+ protected ICommentsManager $commentsManager,
+ protected IUserManager $userManager,
+ protected IUserSession $userSession,
+ protected IEventDispatcher $dispatcher,
+ protected LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/LegacyDAVACL.php b/apps/dav/lib/Connector/LegacyDAVACL.php
index da570b235de..40ce53b8ab0 100644
--- a/apps/dav/lib/Connector/LegacyDAVACL.php
+++ b/apps/dav/lib/Connector/LegacyDAVACL.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector;
diff --git a/apps/dav/lib/Connector/LegacyPublicAuth.php b/apps/dav/lib/Connector/LegacyPublicAuth.php
index 82a474d4bed..03d18853de0 100644
--- a/apps/dav/lib/Connector/LegacyPublicAuth.php
+++ b/apps/dav/lib/Connector/LegacyPublicAuth.php
@@ -1,35 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector;
use OCA\DAV\Connector\Sabre\PublicAuth;
+use OCP\Defaults;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
@@ -47,22 +26,15 @@ class LegacyPublicAuth extends AbstractBasic {
private const BRUTEFORCE_ACTION = 'legacy_public_webdav_auth';
private ?IShare $share = null;
- private IManager $shareManager;
- private ISession $session;
- private IRequest $request;
- private IThrottler $throttler;
-
- public function __construct(IRequest $request,
- IManager $shareManager,
- ISession $session,
- IThrottler $throttler) {
- $this->request = $request;
- $this->shareManager = $shareManager;
- $this->session = $session;
- $this->throttler = $throttler;
+ public function __construct(
+ private IRequest $request,
+ private IManager $shareManager,
+ private ISession $session,
+ private IThrottler $throttler,
+ ) {
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName() ?: 'Nextcloud';
}
diff --git a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
index d8be0c20dc8..0e2b1c58748 100644
--- a/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/AnonymousOptionsPlugin.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
- *
- * @author Bastien Durel <bastien@durel.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
index db0f4e56b2e..9cff113140a 100644
--- a/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/AppleQuirksPlugin.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2023 Claus-Justus Heine
- *
- * @author Claus-Justus Heine <himself@claus-justus-heine.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -75,8 +59,8 @@ class AppleQuirksPlugin extends ServerPlugin {
* This method handles HTTP REPORT requests.
*
* @param string $reportName
- * @param mixed $report
- * @param mixed $path
+ * @param mixed $report
+ * @param mixed $path
*
* @return bool
*/
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index c2e343d8656..a174920946a 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -1,35 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Markus Goetz <markus@woboq.com>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -39,10 +13,13 @@ use OC\Authentication\TwoFactorAuth\Manager;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\AppFramework\Http;
+use OCP\Defaults;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
@@ -52,29 +29,20 @@ use Sabre\HTTP\ResponseInterface;
class Auth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'AUTHENTICATED_TO_DAV_BACKEND';
-
- private ISession $session;
- private Session $userSession;
- private IRequest $request;
private ?string $currentUser = null;
- private Manager $twoFactorManager;
- private IThrottler $throttler;
-
- public function __construct(ISession $session,
- Session $userSession,
- IRequest $request,
- Manager $twoFactorManager,
- IThrottler $throttler,
- string $principalPrefix = 'principals/users/') {
- $this->session = $session;
- $this->userSession = $userSession;
- $this->twoFactorManager = $twoFactorManager;
- $this->request = $request;
- $this->throttler = $throttler;
+
+ public function __construct(
+ private ISession $session,
+ private Session $userSession,
+ private IRequest $request,
+ private Manager $twoFactorManager,
+ private IThrottler $throttler,
+ string $principalPrefix = 'principals/users/',
+ ) {
$this->principalPrefix = $principalPrefix;
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName() ?: 'Nextcloud';
}
@@ -87,8 +55,8 @@ class Auth extends AbstractBasic {
* @see https://github.com/owncloud/core/issues/13245
*/
public function isDavAuthenticated(string $username): bool {
- return !is_null($this->session->get(self::DAV_AUTHENTICATED)) &&
- $this->session->get(self::DAV_AUTHENTICATED) === $username;
+ return !is_null($this->session->get(self::DAV_AUTHENTICATED))
+ && $this->session->get(self::DAV_AUTHENTICATED) === $username;
}
/**
@@ -103,8 +71,8 @@ class Auth extends AbstractBasic {
* @throws PasswordLoginForbidden
*/
protected function validateUserPass($username, $password) {
- if ($this->userSession->isLoggedIn() &&
- $this->isDavAuthenticated($this->userSession->getUser()->getUID())
+ if ($this->userSession->isLoggedIn()
+ && $this->isDavAuthenticated($this->userSession->getUser()->getUID())
) {
$this->session->close();
return true;
@@ -141,7 +109,7 @@ class Auth extends AbstractBasic {
} catch (Exception $e) {
$class = get_class($e);
$msg = $e->getMessage();
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
throw new ServiceUnavailable("$class: $msg");
}
}
@@ -150,8 +118,9 @@ class Auth extends AbstractBasic {
* Checks whether a CSRF check is required on the request
*/
private function requiresCSRFCheck(): bool {
- // GET requires no check at all
- if ($this->request->getMethod() === 'GET') {
+
+ $methodsWithoutCsrf = ['GET', 'HEAD', 'OPTIONS'];
+ if (in_array($this->request->getMethod(), $methodsWithoutCsrf)) {
return false;
}
@@ -175,8 +144,8 @@ class Auth extends AbstractBasic {
}
// If logged-in AND DAV authenticated no check is required
- if ($this->userSession->isLoggedIn() &&
- $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
+ if ($this->userSession->isLoggedIn()
+ && $this->isDavAuthenticated($this->userSession->getUser()->getUID())) {
return false;
}
@@ -190,13 +159,13 @@ class Auth extends AbstractBasic {
private function auth(RequestInterface $request, ResponseInterface $response): array {
$forcedLogout = false;
- if (!$this->request->passesCSRFCheck() &&
- $this->requiresCSRFCheck()) {
+ if (!$this->request->passesCSRFCheck()
+ && $this->requiresCSRFCheck()) {
// In case of a fail with POST we need to recheck the credentials
if ($this->request->getMethod() === 'POST') {
$forcedLogout = true;
} else {
- $response->setStatus(401);
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
throw new \Sabre\DAV\Exception\NotAuthenticated('CSRF check not passed.');
}
}
@@ -209,10 +178,10 @@ class Auth extends AbstractBasic {
}
if (
//Fix for broken webdav clients
- ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED))) ||
+ ($this->userSession->isLoggedIn() && is_null($this->session->get(self::DAV_AUTHENTICATED)))
//Well behaved clients that only send the cookie are allowed
- ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && $request->getHeader('Authorization') === null) ||
- \OC_User::handleApacheAuth()
+ || ($this->userSession->isLoggedIn() && $this->session->get(self::DAV_AUTHENTICATED) === $this->userSession->getUser()->getUID() && empty($request->getHeader('Authorization')))
+ || \OC_User::handleApacheAuth()
) {
$user = $this->userSession->getUser()->getUID();
$this->currentUser = $user;
@@ -221,18 +190,16 @@ class Auth extends AbstractBasic {
}
}
- if (!$this->userSession->isLoggedIn() && in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) {
- // do not re-authenticate over ajax, use dummy auth name to prevent browser popup
- $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"');
- $response->setStatus(401);
- throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
- }
-
$data = parent::check($request, $response);
if ($data[0] === true) {
$startPos = strrpos($data[1], '/') + 1;
$user = $this->userSession->getUser()->getUID();
$data[1] = substr_replace($data[1], $user, $startPos);
+ } elseif (in_array('XMLHttpRequest', explode(',', $request->getHeader('X-Requested-With') ?? ''))) {
+ // For ajax requests use dummy auth name to prevent browser popup in case of invalid creditials
+ $response->addHeader('WWW-Authenticate', 'DummyBasic realm="' . $this->realm . '"');
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
+ throw new \Sabre\DAV\Exception\NotAuthenticated('Cannot authenticate over ajax calls');
}
return $data;
}
diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php
index 6fd61c44b34..23453ae8efb 100644
--- a/apps/dav/lib/Connector/Sabre/BearerAuth.php
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -1,28 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
+use OCP\Defaults;
+use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
@@ -31,22 +17,15 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BearerAuth extends AbstractBearer {
- private IUserSession $userSession;
- private ISession $session;
- private IRequest $request;
- private string $principalPrefix;
-
- public function __construct(IUserSession $userSession,
- ISession $session,
- IRequest $request,
- $principalPrefix = 'principals/users/') {
- $this->userSession = $userSession;
- $this->session = $session;
- $this->request = $request;
- $this->principalPrefix = $principalPrefix;
-
+ public function __construct(
+ private IUserSession $userSession,
+ private ISession $session,
+ private IRequest $request,
+ private IConfig $config,
+ private string $principalPrefix = 'principals/users/',
+ ) {
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName() ?: 'Nextcloud';
}
@@ -81,6 +60,14 @@ class BearerAuth extends AbstractBearer {
* @param ResponseInterface $response
*/
public function challenge(RequestInterface $request, ResponseInterface $response): void {
- $response->setStatus(401);
+ // Legacy ownCloud clients still authenticate via OAuth2
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ $userAgent = $request->getHeader('User-Agent');
+ if ($enableOcClients && $userAgent !== null && str_contains($userAgent, 'mirall')) {
+ parent::challenge($request, $response);
+ return;
+ }
+
+ $response->setStatus(Http::STATUS_UNAUTHORIZED);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
index eda2399a780..21358406a4a 100644
--- a/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/BlockLegacyClientPlugin.php
@@ -1,30 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\Theming\ThemingDefaults;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Server;
@@ -39,10 +22,11 @@ use Sabre\HTTP\RequestInterface;
*/
class BlockLegacyClientPlugin extends ServerPlugin {
protected ?Server $server = null;
- protected IConfig $config;
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private ThemingDefaults $themingDefaults,
+ ) {
}
/**
@@ -65,11 +49,26 @@ class BlockLegacyClientPlugin extends ServerPlugin {
return;
}
- $minimumSupportedDesktopVersion = $this->config->getSystemValue('minimum.supported.desktop.version', '2.3.0');
+ $minimumSupportedDesktopVersion = $this->config->getSystemValueString('minimum.supported.desktop.version', '3.1.0');
+ $maximumSupportedDesktopVersion = $this->config->getSystemValueString('maximum.supported.desktop.version', '99.99.99');
+
+ // Check if the client is a desktop client
preg_match(IRequest::USER_AGENT_CLIENT_DESKTOP, $userAgent, $versionMatches);
- if (isset($versionMatches[1]) &&
- version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
- throw new \Sabre\DAV\Exception\Forbidden('Unsupported client version.');
+
+ // If the client is a desktop client and the version is too old, block it
+ if (isset($versionMatches[1]) && version_compare($versionMatches[1], $minimumSupportedDesktopVersion) === -1) {
+ $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl());
+ $minimumSupportedDesktopVersion = htmlspecialchars($minimumSupportedDesktopVersion);
+
+ throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Upgrade to <a href=\"$customClientDesktopLink\">version $minimumSupportedDesktopVersion or later</a>.");
+ }
+
+ // If the client is a desktop client and the version is too new, block it
+ if (isset($versionMatches[1]) && version_compare($versionMatches[1], $maximumSupportedDesktopVersion) === 1) {
+ $customClientDesktopLink = htmlspecialchars($this->themingDefaults->getSyncClientUrl());
+ $maximumSupportedDesktopVersion = htmlspecialchars($maximumSupportedDesktopVersion);
+
+ throw new \Sabre\DAV\Exception\Forbidden("This version of the client is unsupported. Downgrade to <a href=\"$customClientDesktopLink\">version $maximumSupportedDesktopVersion or earlier</a>.");
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/CachingTree.php b/apps/dav/lib/Connector/Sabre/CachingTree.php
index 54038468dc5..5d72b530f58 100644
--- a/apps/dav/lib/Connector/Sabre/CachingTree.php
+++ b/apps/dav/lib/Connector/Sabre/CachingTree.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -45,7 +28,7 @@ class CachingTree extends Tree {
// flushing the entire cache
$path = trim($path, '/');
foreach ($this->cache as $nodePath => $node) {
- $nodePath = (string) $nodePath;
+ $nodePath = (string)$nodePath;
if ($path === '' || $nodePath == $path || str_starts_with($nodePath, $path . '/')) {
unset($this->cache[$nodePath]);
}
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php
index 8e01dcc0f4b..75d1d718de1 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumList.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
index d68ced83616..18009080585 100644
--- a/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ChecksumUpdatePlugin.php
@@ -2,27 +2,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -41,20 +27,6 @@ class ChecksumUpdatePlugin extends ServerPlugin {
}
/** @return string[] */
- public function getHTTPMethods($path): array {
- $tree = $this->server->tree;
-
- if ($tree->nodeExists($path)) {
- $node = $tree->getNodeForPath($path);
- if ($node instanceof File) {
- return ['PATCH'];
- }
- }
-
- return [];
- }
-
- /** @return string[] */
public function getFeatures(): array {
return ['nextcloud-checksum-update'];
}
@@ -74,7 +46,7 @@ class ChecksumUpdatePlugin extends ServerPlugin {
$node->setChecksum($checksum);
$response->addHeader('OC-Checksum', $checksum);
$response->setHeader('Content-Length', '0');
- $response->setStatus(204);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
return false;
}
diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
index 94f9e167aae..e4b6c2636da 100644
--- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php
@@ -2,28 +2,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -39,13 +20,12 @@ class CommentPropertiesPlugin extends ServerPlugin {
public const PROPERTY_NAME_UNREAD = '{http://owncloud.org/ns}comments-unread';
protected ?Server $server = null;
- private ICommentsManager $commentsManager;
- private IUserSession $userSession;
private array $cachedUnreadCount = [];
- public function __construct(ICommentsManager $commentsManager, IUserSession $userSession) {
- $this->commentsManager = $commentsManager;
- $this->userSession = $userSession;
+ public function __construct(
+ private ICommentsManager $commentsManager,
+ private IUserSession $userSession,
+ ) {
}
/**
@@ -81,7 +61,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
$ids[] = (string)$id;
}
- $ids[] = (string) $directory->getId();
+ $ids[] = (string)$directory->getId();
$unread = $this->commentsManager->getNumberOfUnreadCommentsForObjects('files', $ids, $this->userSession->getUser());
foreach ($unread as $id => $count) {
@@ -99,7 +79,7 @@ class CommentPropertiesPlugin extends ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
if (!($node instanceof File) && !($node instanceof Directory)) {
return;
diff --git a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
index 0533dcab4d9..609ac295b4c 100644
--- a/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/CopyEtagHeaderPlugin.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
index 61fac7250bc..100d719ef01 100644
--- a/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DavAclPlugin.php
@@ -1,35 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCA\DAV\CalDAV\CachedSubscription;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CardDAV\AddressBook;
use Sabre\CalDAV\Principal\User;
+use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
@@ -61,19 +43,26 @@ class DavAclPlugin extends \Sabre\DAVACL\Plugin {
$type = 'Addressbook';
break;
case Calendar::class:
+ case CachedSubscription::class:
$type = 'Calendar';
break;
default:
$type = 'Node';
break;
}
- throw new NotFound(
- sprintf(
- "%s with name '%s' could not be found",
- $type,
- $node->getName()
- )
- );
+
+ if ($this->getCurrentUserPrincipal() === $node->getOwner()) {
+ throw new Forbidden('Access denied');
+ } else {
+ throw new NotFound(
+ sprintf(
+ "%s with name '%s' could not be found",
+ $type,
+ $node->getName()
+ )
+ );
+ }
+
}
return $access;
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index e40b8d760eb..fe09c3f423f 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -1,34 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -38,10 +13,15 @@ use OCA\DAV\AppInfo\Application;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\DAV\Storage\PublicShareWrapper;
+use OCP\App\IAppManager;
+use OCP\Constants;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\ForbiddenException;
use OCP\Files\InvalidPathException;
+use OCP\Files\Mount\IMountManager;
+use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\StorageNotAvailableException;
use OCP\IL10N;
@@ -49,6 +29,7 @@ use OCP\IRequest;
use OCP\L10N\IFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
+use OCP\Server;
use OCP\Share\IManager as IShareManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
@@ -58,23 +39,26 @@ use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
use Sabre\DAV\INode;
-class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
+class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
/**
* Cached directory content
- * @var \OCP\Files\FileInfo[]
+ * @var FileInfo[]
*/
private ?array $dirContent = null;
/** Cached quota info */
private ?array $quotaInfo = null;
- private ?CachingTree $tree = null;
/**
* Sets up the node, expects a full path name
*/
- public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, ?IShareManager $shareManager = null) {
+ public function __construct(
+ View $view,
+ FileInfo $info,
+ private ?CachingTree $tree = null,
+ ?IShareManager $shareManager = null,
+ ) {
parent::__construct($view, $info, $shareManager);
- $this->tree = $tree;
}
/**
@@ -111,21 +95,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
*/
public function createFile($name, $data = null) {
try {
- // for chunked upload also updating a existing file is a "createFile"
- // because we create all the chunks before re-assemble them to the existing file.
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- // exit if we can't create a new file and we don't updatable existing file
- $chunkInfo = \OC_FileChunking::decodeName($name);
- if (!$this->fileView->isCreatable($this->path) &&
- !$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
- ) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
- } else {
- // For non-chunked upload it is enough to check if we can create a new file
- if (!$this->fileView->isCreatable($this->path)) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
+ if (!$this->fileView->isCreatable($this->path)) {
+ throw new \Sabre\DAV\Exception\Forbidden();
}
$this->fileView->verifyPath($this->path, $name);
@@ -139,18 +110,18 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
'type' => FileInfo::TYPE_FILE
], null);
}
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ $node = new File($this->fileView, $info);
// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
$node->acquireLock(ILockingProvider::LOCK_SHARED);
- $this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
+ $this->fileView->lockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$result = $node->put($data);
- $this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
+ $this->fileView->unlockFile($this->path . '/' . $name . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$node->releaseLock(ILockingProvider::LOCK_SHARED);
return $result;
- } catch (\OCP\Files\StorageNotAvailableException $e) {
+ } catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage(), false, $ex);
@@ -181,12 +152,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if (!$this->fileView->mkdir($newPath)) {
throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
}
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
} catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
+ throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $ex) {
- throw new Forbidden($ex->getMessage(), $ex->getRetry());
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -196,14 +167,27 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* Returns a specific child node, referenced by its name
*
* @param string $name
- * @param \OCP\Files\FileInfo $info
+ * @param FileInfo $info
* @return \Sabre\DAV\INode
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\NotFound
* @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getChild($name, $info = null, ?IRequest $request = null, ?IL10N $l10n = null) {
- if (!$this->info->isReadable()) {
+ $storage = $this->info->getStorage();
+ $allowDirectory = false;
+
+ // Checking if we're in a file drop
+ // If we are, then only PUT and MKCOL are allowed (see plugin)
+ // so we are safe to return the directory without a risk of
+ // leaking files and folders structure.
+ if ($storage instanceof PublicShareWrapper) {
+ $share = $storage->getShare();
+ $allowDirectory = ($share->getPermissions() & Constants::PERMISSION_READ) !== Constants::PERMISSION_READ;
+ }
+
+ // For file drop we need to be allowed to read the directory with the nickname
+ if (!$allowDirectory && !$this->info->isReadable()) {
// avoid detecting files through this way
throw new NotFound();
}
@@ -211,14 +195,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$path = $this->path . '/' . $name;
if (is_null($info)) {
try {
- $this->fileView->verifyPath($this->path, $name);
+ $this->fileView->verifyPath($this->path, $name, true);
$info = $this->fileView->getFileInfo($path);
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
+ } catch (StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
} catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
+ throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $e) {
- throw new \Sabre\DAV\Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
}
}
@@ -229,7 +213,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
} else {
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n);
+ // In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
+ if (!$this->info->isReadable()) {
+ throw new NotFound();
+ }
+
+ $node = new File($this->fileView, $info, $this->shareManager, $request, $l10n);
}
if ($this->tree) {
$this->tree->cacheNode($node);
@@ -242,7 +231,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
*
* @return \Sabre\DAV\INode[]
* @throws \Sabre\DAV\Exception\Locked
- * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
+ * @throws Forbidden
*/
public function getChildren() {
if (!is_null($this->dirContent)) {
@@ -252,7 +241,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if (!$this->info->isReadable()) {
// return 403 instead of 404 because a 404 would make
// the caller believe that the collection itself does not exist
- if (\OCP\Server::get(\OCP\App\IAppManager::class)->isInstalled('files_accesscontrol')) {
+ if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
} else {
throw new Forbidden('No read permissions');
@@ -264,8 +253,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$nodes = [];
- $request = \OC::$server->get(IRequest::class);
- $l10nFactory = \OC::$server->get(IFactory::class);
+ $request = Server::get(IRequest::class);
+ $l10nFactory = Server::get(IFactory::class);
$l10n = $l10nFactory->get(Application::APP_ID);
foreach ($folderContent as $info) {
$node = $this->getChild($info->getName(), $info, $request, $l10n);
@@ -318,7 +307,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
private function getLogger(): LoggerInterface {
- return \OC::$server->get(LoggerInterface::class);
+ return Server::get(LoggerInterface::class);
}
/**
@@ -332,14 +321,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
}
$relativePath = $this->fileView->getRelativePath($this->info->getPath());
if ($relativePath === null) {
- $this->getLogger()->warning("error while getting quota as the relative path cannot be found");
+ $this->getLogger()->warning('error while getting quota as the relative path cannot be found');
return [0, 0];
}
try {
$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
- if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
- $free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
+ if ($storageInfo['quota'] === FileInfo::SPACE_UNLIMITED) {
+ $free = FileInfo::SPACE_UNLIMITED;
} else {
$free = $storageInfo['free'];
}
@@ -348,14 +337,14 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$free
];
return $this->quotaInfo;
- } catch (\OCP\Files\NotFoundException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ } catch (NotFoundException $e) {
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
- } catch (\OCP\Files\StorageNotAvailableException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ } catch (StorageNotAvailableException $e) {
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
} catch (NotPermittedException $e) {
- $this->getLogger()->warning("error while getting quota into", ['exception' => $e]);
+ $this->getLogger()->warning('error while getting quota into', ['exception' => $e]);
return [0, 0];
}
}
@@ -395,10 +384,6 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
throw new BadRequest('Incompatible node types');
}
- if (!$this->fileView) {
- throw new ServiceUnavailable('filesystem not setup');
- }
-
$destinationPath = $this->getPath() . '/' . $targetName;
@@ -416,7 +401,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$sourcePath = $sourceNode->getPath();
$isMovableMount = false;
- $sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
+ $sourceMount = Server::get(IMountManager::class)->find($this->fileView->getAbsolutePath($sourcePath));
$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
if ($sourceMount instanceof MoveableMount && $internalPath === '') {
$isMovableMount = true;
@@ -456,9 +441,9 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
throw new \Sabre\DAV\Exception\Forbidden('');
}
} catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (ForbiddenException $ex) {
- throw new Forbidden($ex->getMessage(), $ex->getRetry());
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@@ -469,20 +454,34 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
public function copyInto($targetName, $sourcePath, INode $sourceNode) {
if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
- $destinationPath = $this->getPath() . '/' . $targetName;
- $sourcePath = $sourceNode->getPath();
+ try {
+ $destinationPath = $this->getPath() . '/' . $targetName;
+ $sourcePath = $sourceNode->getPath();
- if (!$this->fileView->isCreatable($this->getPath())) {
- throw new \Sabre\DAV\Exception\Forbidden();
- }
+ if (!$this->fileView->isCreatable($this->getPath())) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
- try {
- $this->fileView->verifyPath($this->getPath(), $targetName);
- } catch (InvalidPathException $ex) {
- throw new InvalidPath($ex->getMessage());
- }
+ try {
+ $this->fileView->verifyPath($this->getPath(), $targetName);
+ } catch (InvalidPathException $ex) {
+ throw new InvalidPath($ex->getMessage());
+ }
- return $this->fileView->copy($sourcePath, $destinationPath);
+ $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
+
+ if (!$copyOkay) {
+ throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
+ }
+
+ return true;
+ } catch (StorageNotAvailableException $e) {
+ throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
+ } catch (ForbiddenException $ex) {
+ throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
+ } catch (LockedException $e) {
+ throw new FileLocked($e->getMessage(), $e->getCode(), $e);
+ }
}
return false;
diff --git a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
index 2019c77ad35..f6baceb748b 100644
--- a/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/DummyGetResponsePlugin.php
@@ -1,31 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wickert <cwickert@suse.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -61,13 +43,13 @@ class DummyGetResponsePlugin extends \Sabre\DAV\ServerPlugin {
* @return false
*/
public function httpGet(RequestInterface $request, ResponseInterface $response) {
- $string = 'This is the WebDAV interface. It can only be accessed by ' .
- 'WebDAV clients such as the Nextcloud desktop sync client.';
+ $string = 'This is the WebDAV interface. It can only be accessed by '
+ . 'WebDAV clients such as the Nextcloud desktop sync client.';
$stream = fopen('php://memory', 'r+');
fwrite($stream, $string);
rewind($stream);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($stream);
return false;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
index c4cd6db190a..1e1e4aaed04 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/BadGateway.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
index 4fc3399ca81..60b3b06ea01 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/EntityTooLarge.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
index 83e01f290b8..38708e945e9 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/FileLocked.php
@@ -1,40 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Owen Winkler <a_github@midnightcircus.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
use Exception;
+use OCP\Files\LockNotAcquiredException;
class FileLocked extends \Sabre\DAV\Exception {
/**
* @param string $message
* @param int $code
*/
- public function __construct($message = "", $code = 0, ?Exception $previous = null) {
- if ($previous instanceof \OCP\Files\LockNotAcquiredException) {
+ public function __construct($message = '', $code = 0, ?Exception $previous = null) {
+ if ($previous instanceof LockNotAcquiredException) {
$message = sprintf('Target file %s is locked by another process.', $previous->path);
}
parent::__construct($message, $code, $previous);
diff --git a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
index d71b837ade2..95d4b3ab514 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
@@ -26,18 +11,16 @@ class Forbidden extends \Sabre\DAV\Exception\Forbidden {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * @var bool
- */
- private $retry;
-
- /**
* @param string $message
* @param bool $retry
* @param \Exception $previous
*/
- public function __construct($message, $retry = false, ?\Exception $previous = null) {
+ public function __construct(
+ $message,
+ private $retry = false,
+ ?\Exception $previous = null,
+ ) {
parent::__construct($message, 0, $previous);
- $this->retry = $retry;
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
index a8ca0aea97d..dfc08aa8b88 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
@@ -29,18 +13,16 @@ class InvalidPath extends Exception {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * @var bool
- */
- private $retry;
-
- /**
* @param string $message
* @param bool $retry
* @param \Exception|null $previous
*/
- public function __construct($message, $retry = false, ?\Exception $previous = null) {
+ public function __construct(
+ $message,
+ private $retry = false,
+ ?\Exception $previous = null,
+ ) {
parent::__construct($message, 0, $previous);
- $this->retry = $retry;
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
index 7c00d15f627..f5cc117fafc 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/PasswordLoginForbidden.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
index 1110797aed4..67455fc9474 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/TooManyRequests.php
@@ -2,25 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
index a7e935d2497..c5fbfa3a16c 100644
--- a/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
+++ b/apps/dav/lib/Connector/Sabre/Exception/UnsupportedMediaType.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre\Exception;
diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
index a195b5722f5..686386dbfef 100644
--- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php
@@ -1,29 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -65,6 +45,9 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
// forbidden can be expected when trying to upload to
// read-only folders for example
Forbidden::class => true,
+ // our forbidden is expected when access control is blocking
+ // an item in a folder
+ \OCA\DAV\Connector\Sabre\Exception\Forbidden::class => true,
// Happens when an external storage or federated share is temporarily
// not available
StorageNotAvailableException::class => true,
@@ -84,15 +67,13 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
ServerMaintenanceMode::class => true,
];
- private string $appName;
- private LoggerInterface $logger;
-
/**
- * @param string $loggerAppName app name to use when logging
+ * @param string $appName app name to use when logging
*/
- public function __construct(string $loggerAppName, LoggerInterface $logger) {
- $this->appName = $loggerAppName;
- $this->logger = $logger;
+ public function __construct(
+ private string $appName,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
index c63b1cf50f2..b0c5a079ce1 100644
--- a/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php
@@ -1,33 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\AppFramework\Http;
use Sabre\DAV\INode;
use Sabre\DAV\Locks\LockInfo;
use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Xml\Property\LockDiscovery;
use Sabre\DAV\Xml\Property\SupportedLock;
@@ -47,11 +31,11 @@ use Sabre\HTTP\ResponseInterface;
* @package OCA\DAV\Connector\Sabre
*/
class FakeLockerPlugin extends ServerPlugin {
- /** @var \Sabre\DAV\Server */
+ /** @var Server */
private $server;
/** {@inheritDoc} */
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(Server $server) {
$this->server = $server;
$this->server->on('method:LOCK', [$this, 'fakeLockProvider'], 1);
$this->server->on('method:UNLOCK', [$this, 'fakeUnlockProvider'], 1);
@@ -129,15 +113,15 @@ class FakeLockerPlugin extends ServerPlugin {
$lockInfo = new LockInfo();
$lockInfo->token = md5($request->getPath());
$lockInfo->uri = $request->getPath();
- $lockInfo->depth = \Sabre\DAV\Server::DEPTH_INFINITY;
+ $lockInfo->depth = Server::DEPTH_INFINITY;
$lockInfo->timeout = 1800;
$body = $this->server->xml->write('{DAV:}prop', [
- '{DAV:}lockdiscovery' =>
- new LockDiscovery([$lockInfo])
+ '{DAV:}lockdiscovery'
+ => new LockDiscovery([$lockInfo])
]);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setBody($body);
return false;
@@ -152,7 +136,7 @@ class FakeLockerPlugin extends ServerPlugin {
*/
public function fakeUnlockProvider(RequestInterface $request,
ResponseInterface $response) {
- $response->setStatus(204);
+ $response->setStatus(Http::STATUS_NO_CONTENT);
$response->setHeader('Content-Length', '0');
return false;
}
diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php
index f54fbbdd15a..d2a71eb3e7b 100644
--- a/apps/dav/lib/Connector/Sabre/File.php
+++ b/apps/dav/lib/Connector/Sabre/File.php
@@ -1,40 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Jan-Philipp Litza <jplitza@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Owen Winkler <a_github@midnightcircus.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Semih Serhat Karakaya <karakayasemi@itu.edu.tr>
- * @author Stefan Schneider <stefan.schneider@squareweave.com.au>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -44,35 +13,38 @@ use OC\Files\Filesystem;
use OC\Files\Stream\HashWrapper;
use OC\Files\View;
use OCA\DAV\AppInfo\Application;
-use OCA\DAV\Connector\Sabre\Exception\BadGateway;
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
+use OCP\App\IAppManager;
use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\Files;
use OCP\Files\EntityTooLargeException;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
+use OCP\Files\IMimeTypeDetector;
use OCP\Files\InvalidContentException;
use OCP\Files\InvalidPathException;
use OCP\Files\LockNotAcquiredException;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
-use OCP\Files\Storage;
+use OCP\Files\Storage\IWriteStreamStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\L10N\IFactory as IL10NFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
+use OCP\Server;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
-use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\IFile;
@@ -83,8 +55,8 @@ class File extends Node implements IFile {
/**
* Sets up the node, expects a full path name
*
- * @param \OC\Files\View $view
- * @param \OCP\Files\FileInfo $info
+ * @param View $view
+ * @param FileInfo $info
* @param ?\OCP\Share\IManager $shareManager
* @param ?IRequest $request
* @param ?IL10N $l10n
@@ -97,14 +69,14 @@ class File extends Node implements IFile {
} else {
// Querying IL10N directly results in a dependency loop
/** @var IL10NFactory $l10nFactory */
- $l10nFactory = \OC::$server->get(IL10NFactory::class);
+ $l10nFactory = Server::get(IL10NFactory::class);
$this->l10n = $l10nFactory->get(Application::APP_ID);
}
if (isset($request)) {
$this->request = $request;
} else {
- $this->request = \OC::$server->get(IRequest::class);
+ $this->request = Server::get(IRequest::class);
}
}
@@ -125,7 +97,7 @@ class File extends Node implements IFile {
* different object on a subsequent GET you are strongly recommended to not
* return an ETag, and just return null.
*
- * @param resource $data
+ * @param resource|string $data
*
* @throws Forbidden
* @throws UnsupportedMediaType
@@ -149,25 +121,18 @@ class File extends Node implements IFile {
// verify path of the target
$this->verifyPath();
- // chunked handling
- $chunkedHeader = $this->request->getHeader('oc-chunked');
- if ($chunkedHeader) {
- try {
- return $this->createFileChunked($data);
- } catch (\Exception $e) {
- $this->convertToSabreException($e);
- }
- }
-
- /** @var Storage $partStorage */
[$partStorage] = $this->fileView->resolvePath($this->path);
+ if ($partStorage === null) {
+ throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
+ }
$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
- $view = \OC\Files\Filesystem::getView();
+ $view = Filesystem::getView();
if ($needsPartFile) {
+ $transferId = \rand();
// mark file as partial while uploading (ignored by the scanner)
- $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
+ $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
$needsPartFile = false;
@@ -183,10 +148,11 @@ class File extends Node implements IFile {
}
// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
- /** @var \OC\Files\Storage\Storage $partStorage */
[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
- /** @var \OC\Files\Storage\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
+ if ($partStorage === null || $storage === null) {
+ throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
+ }
try {
if (!$needsPartFile) {
try {
@@ -220,100 +186,92 @@ class File extends Node implements IFile {
if ($this->request->getHeader('X-HASH') !== '') {
$hash = $this->request->getHeader('X-HASH');
if ($hash === 'all' || $hash === 'md5') {
- $data = HashWrapper::wrap($data, 'md5', function ($hash) {
+ $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
$this->header('X-Hash-MD5: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha1') {
- $data = HashWrapper::wrap($data, 'sha1', function ($hash) {
+ $data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
$this->header('X-Hash-SHA1: ' . $hash);
});
}
if ($hash === 'all' || $hash === 'sha256') {
- $data = HashWrapper::wrap($data, 'sha256', function ($hash) {
+ $data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
$this->header('X-Hash-SHA256: ' . $hash);
});
}
}
- if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
+ $lengthHeader = $this->request->getHeader('content-length');
+ $expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
+
+ if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
$isEOF = false;
- $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
+ $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
$isEOF = feof($stream);
});
- $result = true;
- $count = -1;
- try {
- $count = $partStorage->writeStream($internalPartPath, $wrappedData);
- } catch (GenericFileException $e) {
- $result = false;
- } catch (BadGateway $e) {
- throw $e;
- }
-
-
- if ($result === false) {
- $result = $isEOF;
- if (is_resource($wrappedData)) {
- $result = feof($wrappedData);
+ $result = is_resource($wrappedData);
+ if ($result) {
+ $count = -1;
+ try {
+ /** @var IWriteStreamStorage $partStorage */
+ $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
+ } catch (GenericFileException $e) {
+ $logger = Server::get(LoggerInterface::class);
+ $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
+ $result = $isEOF;
+ if (is_resource($wrappedData)) {
+ $result = feof($wrappedData);
+ }
}
}
} else {
$target = $partStorage->fopen($internalPartPath, 'wb');
if ($target === false) {
- \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
+ Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
// because we have no clue about the cause we can only throw back a 500/Internal Server Error
throw new Exception($this->l10n->t('Could not write file contents'));
}
- [$count, $result] = \OC_Helper::streamCopy($data, $target);
+ [$count, $result] = Files::streamCopy($data, $target, true);
fclose($target);
}
-
- if ($result === false) {
- $expected = -1;
- $lengthHeader = $this->request->getHeader('content-length');
- if ($lengthHeader) {
- $expected = (int)$lengthHeader;
- }
- if ($expected !== 0) {
- throw new Exception(
- $this->l10n->t(
- 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
- [
- $this->l10n->n('%n byte', '%n bytes', $count),
- $this->l10n->n('%n byte', '%n bytes', $expected),
- ],
- )
- );
- }
+ if ($result === false && $expected !== null) {
+ throw new Exception(
+ $this->l10n->t(
+ 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
+ [
+ $this->l10n->n('%n byte', '%n bytes', $count),
+ $this->l10n->n('%n byte', '%n bytes', $expected),
+ ],
+ )
+ );
}
// if content length is sent by client:
// double check if the file was fully received
// compare expected and actual size
- $lengthHeader = $this->request->getHeader('content-length');
- if ($lengthHeader && $this->request->getMethod() === 'PUT') {
- $expected = (int)$lengthHeader;
- if ($count !== $expected) {
- throw new BadRequest(
- $this->l10n->t(
- 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
- [
- $this->l10n->n('%n byte', '%n bytes', $expected),
- $this->l10n->n('%n byte', '%n bytes', $count),
- ],
- )
- );
- }
+ if ($expected !== null
+ && $expected !== $count
+ && $this->request->getMethod() === 'PUT'
+ ) {
+ throw new BadRequest(
+ $this->l10n->t(
+ 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
+ [
+ $this->l10n->n('%n byte', '%n bytes', $expected),
+ $this->l10n->n('%n byte', '%n bytes', $count),
+ ],
+ )
+ );
}
} catch (\Exception $e) {
if ($e instanceof LockedException) {
- \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
} else {
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
+ Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
if ($needsPartFile) {
@@ -354,7 +312,7 @@ class File extends Node implements IFile {
$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
$fileExists = $storage->file_exists($internalPath);
if ($renameOkay === false || $fileExists === false) {
- \OC::$server->get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
+ Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
throw new Exception($this->l10n->t('Could not rename part file to final file'));
}
} catch (ForbiddenException $ex) {
@@ -421,11 +379,16 @@ class File extends Node implements IFile {
}
private function getPartFileBasePath($path) {
- $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
+ $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
if ($partFileInStorage) {
- return $path;
+ $filename = basename($path);
+ // hash does not need to be secure but fast and semi unique
+ $hashedFilename = hash('xxh128', $filename);
+ return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
} else {
- return md5($path); // will place it in the root of the view with a unique name
+ // will place the .part file in the users root directory
+ // therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
+ return hash('xxh128', $path);
}
}
@@ -441,19 +404,19 @@ class File extends Node implements IFile {
$run = true;
if (!$exists) {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
} else {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
}
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [
- \OC\Files\Filesystem::signal_param_path => $hookPath,
- \OC\Files\Filesystem::signal_param_run => &$run,
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
+ Filesystem::signal_param_path => $hookPath,
+ Filesystem::signal_param_run => &$run,
]);
return $run;
}
@@ -468,16 +431,16 @@ class File extends Node implements IFile {
return;
}
if (!$exists) {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
+ Filesystem::signal_param_path => $hookPath
]);
} else {
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
+ Filesystem::signal_param_path => $hookPath
]);
}
- \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [
- \OC\Files\Filesystem::signal_param_path => $hookPath
+ \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
+ Filesystem::signal_param_path => $hookPath
]);
}
@@ -495,20 +458,25 @@ class File extends Node implements IFile {
// do a if the file did not exist
throw new NotFound();
}
+ $path = ltrim($this->path, '/');
try {
- $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
+ $res = $this->fileView->fopen($path, 'rb');
} catch (\Exception $e) {
$this->convertToSabreException($e);
}
if ($res === false) {
- throw new ServiceUnavailable($this->l10n->t('Could not open file'));
+ if ($this->fileView->file_exists($path)) {
+ throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
+ } else {
+ throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
+ }
}
// comparing current file size with the one in DB
// if different, fix DB and refresh cache.
if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
- $logger = \OC::$server->get(LoggerInterface::class);
+ $logger = Server::get(LoggerInterface::class);
$logger->warning('fixing cached size of file id=' . $this->getId());
$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
@@ -567,17 +535,16 @@ class File extends Node implements IFile {
if ($this->request->getMethod() === 'PROPFIND') {
return $mimeType;
}
- return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
+ return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
}
/**
* @return array|bool
*/
public function getDirectDownload() {
- if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) {
+ if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
return [];
}
- /** @var \OCP\Files\Storage $storage */
[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
if (is_null($storage)) {
return [];
@@ -587,135 +554,6 @@ class File extends Node implements IFile {
}
/**
- * @param resource $data
- * @return null|string
- * @throws Exception
- * @throws BadRequest
- * @throws NotImplemented
- * @throws ServiceUnavailable
- */
- private function createFileChunked($data) {
- [$path, $name] = \Sabre\Uri\split($this->path);
-
- $info = \OC_FileChunking::decodeName($name);
- if (empty($info)) {
- throw new NotImplemented($this->l10n->t('Invalid chunk name'));
- }
-
- $chunk_handler = new \OC_FileChunking($info);
- $bytesWritten = $chunk_handler->store($info['index'], $data);
-
- //detect aborted upload
- if ($this->request->getMethod() === 'PUT') {
- $lengthHeader = $this->request->getHeader('content-length');
- if ($lengthHeader) {
- $expected = (int)$lengthHeader;
- if ($bytesWritten !== $expected) {
- $chunk_handler->remove($info['index']);
- throw new BadRequest(
- $this->l10n->t(
- 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
- [
- $this->l10n->n('%n byte', '%n bytes', $expected),
- $this->l10n->n('%n byte', '%n bytes', $bytesWritten),
- ],
- )
- );
- }
- }
- }
-
- if ($chunk_handler->isComplete()) {
- /** @var Storage $storage */
- [$storage,] = $this->fileView->resolvePath($path);
- $needsPartFile = $storage->needsPartFile();
- $partFile = null;
-
- $targetPath = $path . '/' . $info['name'];
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
-
- $exists = $this->fileView->file_exists($targetPath);
-
- try {
- $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
-
- $this->emitPreHooks($exists, $targetPath);
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
-
- if ($needsPartFile) {
- // we first assembly the target file as a part file
- $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
- /** @var \OC\Files\Storage\Storage $targetStorage */
- [$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
-
-
- $chunk_handler->file_assemble($partStorage, $partInternalPath);
-
- // here is the final atomic rename
- $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
- $fileExists = $targetStorage->file_exists($targetInternalPath);
- if ($renameOkay === false || $fileExists === false) {
- \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
- // only delete if an error occurred and the target file was already created
- if ($fileExists) {
- // set to null to avoid double-deletion when handling exception
- // stray part file
- $partFile = null;
- $targetStorage->unlink($targetInternalPath);
- }
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
- throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
- }
- } else {
- // assemble directly into the final file
- $chunk_handler->file_assemble($targetStorage, $targetInternalPath);
- }
-
- // allow sync clients to send the mtime along in a header
- $mtimeHeader = $this->request->getHeader('x-oc-mtime');
- if ($mtimeHeader !== '') {
- $mtime = $this->sanitizeMtime($mtimeHeader);
- if ($targetStorage->touch($targetInternalPath, $mtime)) {
- $this->header('X-OC-MTime: accepted');
- }
- }
-
- // since we skipped the view we need to scan and emit the hooks ourselves
- $targetStorage->getUpdater()->update($targetInternalPath);
-
- $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
-
- $this->emitPostHooks($exists, $targetPath);
-
- // FIXME: should call refreshInfo but can't because $this->path is not the of the final file
- $info = $this->fileView->getFileInfo($targetPath);
-
- $checksumHeader = $this->request->getHeader('oc-checksum');
- if ($checksumHeader) {
- $checksum = trim($checksumHeader);
- $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
- } elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
- $this->fileView->putFileInfo($this->path, ['checksum' => '']);
- }
-
- $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
-
- return $info->getEtag();
- } catch (\Exception $e) {
- if ($partFile !== null) {
- $targetStorage->unlink($targetInternalPath);
- }
- $this->convertToSabreException($e);
- }
- }
-
- return null;
- }
-
- /**
* Convert the given exception to a SabreException instance
*
* @param \Exception $e
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index 7c9c7099878..843383a0452 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -1,42 +1,23 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\AppFramework\Http\Request;
+use OC\FilesMetadata\Model\FilesMetadata;
+use OC\User\NoUserException;
+use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCA\Files_Sharing\External\Mount as SharingExternalMount;
+use OCP\Accounts\IAccountManager;
use OCP\Constants;
use OCP\Files\ForbiddenException;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\InvalidPathException;
+use OCP\Files\Storage\ISharedStorage;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\Exceptions\FilesMetadataException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
@@ -46,6 +27,7 @@ use OCP\IConfig;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
+use OCP\L10N\IFactory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
@@ -80,11 +62,12 @@ class FilesPlugin extends ServerPlugin {
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
- public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
+ public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time';
public const SHARE_NOTE = '{http://nextcloud.org/ns}note';
+ public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download';
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-';
@@ -92,33 +75,28 @@ class FilesPlugin extends ServerPlugin {
/** Reference to main server object */
private ?Server $server = null;
- private Tree $tree;
- private IUserSession $userSession;
/**
- * Whether this is public webdav.
- * If true, some returned information will be stripped off.
+ * @param Tree $tree
+ * @param IConfig $config
+ * @param IRequest $request
+ * @param IPreview $previewManager
+ * @param IUserSession $userSession
+ * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off.
+ * @param bool $downloadAttachment
+ * @return void
*/
- private bool $isPublic;
- private bool $downloadAttachment;
- private IConfig $config;
- private IRequest $request;
- private IPreview $previewManager;
-
- public function __construct(Tree $tree,
- IConfig $config,
- IRequest $request,
- IPreview $previewManager,
- IUserSession $userSession,
- bool $isPublic = false,
- bool $downloadAttachment = true) {
- $this->tree = $tree;
- $this->config = $config;
- $this->request = $request;
- $this->userSession = $userSession;
- $this->isPublic = $isPublic;
- $this->downloadAttachment = $downloadAttachment;
- $this->previewManager = $previewManager;
+ public function __construct(
+ private Tree $tree,
+ private IConfig $config,
+ private IRequest $request,
+ private IPreview $previewManager,
+ private IUserSession $userSession,
+ private IFilenameValidator $validator,
+ private IAccountManager $accountManager,
+ private bool $isPublic = false,
+ private bool $downloadAttachment = true,
+ ) {
}
/**
@@ -148,7 +126,7 @@ class FilesPlugin extends ServerPlugin {
$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
- $server->protectedProperties[] = self::IS_ENCRYPTED_PROPERTYNAME;
+ $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_NOTE;
// normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH
@@ -162,40 +140,79 @@ class FilesPlugin extends ServerPlugin {
$this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']);
$this->server->on('afterMethod:GET', [$this,'httpGet']);
$this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']);
- $this->server->on('afterResponse', function ($request, ResponseInterface $response) {
+ $this->server->on('afterResponse', function ($request, ResponseInterface $response): void {
$body = $response->getBody();
if (is_resource($body)) {
fclose($body);
}
});
$this->server->on('beforeMove', [$this, 'checkMove']);
+ $this->server->on('beforeCopy', [$this, 'checkCopy']);
}
/**
- * Plugin that checks if a move can actually be performed.
+ * Plugin that checks if a copy can actually be performed.
*
* @param string $source source path
- * @param string $destination destination path
- * @throws Forbidden
- * @throws NotFound
+ * @param string $target target path
+ * @throws NotFound If the source does not exist
+ * @throws InvalidPath If the target is invalid
*/
- public function checkMove($source, $destination) {
+ public function checkCopy($source, $target): void {
$sourceNode = $this->tree->getNodeForPath($source);
if (!$sourceNode instanceof Node) {
return;
}
- [$sourceDir,] = \Sabre\Uri\split($source);
- [$destinationDir,] = \Sabre\Uri\split($destination);
- if ($sourceDir !== $destinationDir) {
- $sourceNodeFileInfo = $sourceNode->getFileInfo();
- if ($sourceNodeFileInfo === null) {
- throw new NotFound($source . ' does not exist');
+ // Ensure source exists
+ $sourceNodeFileInfo = $sourceNode->getFileInfo();
+ if ($sourceNodeFileInfo === null) {
+ throw new NotFound($source . ' does not exist');
+ }
+ // Ensure the target name is valid
+ try {
+ [$targetPath, $targetName] = \Sabre\Uri\split($target);
+ $this->validator->validateFilename($targetName);
+ } catch (InvalidPathException $e) {
+ throw new InvalidPath($e->getMessage(), false);
+ }
+ // Ensure the target path is valid
+ $segments = array_slice(explode('/', $targetPath), 2);
+ foreach ($segments as $segment) {
+ if ($this->validator->isFilenameValid($segment) === false) {
+ $l = \OCP\Server::get(IFactory::class)->get('dav');
+ throw new InvalidPath($l->t('Invalid target path'));
}
+ }
+ }
- if (!$sourceNodeFileInfo->isDeletable()) {
- throw new Forbidden($source . " cannot be deleted");
- }
+ /**
+ * Plugin that checks if a move can actually be performed.
+ *
+ * @param string $source source path
+ * @param string $target target path
+ * @throws Forbidden If the source is not deletable
+ * @throws NotFound If the source does not exist
+ * @throws InvalidPath If the target name is invalid
+ */
+ public function checkMove(string $source, string $target): void {
+ $sourceNode = $this->tree->getNodeForPath($source);
+ if (!$sourceNode instanceof Node) {
+ return;
+ }
+
+ // First check copyable (move only needs additional delete permission)
+ $this->checkCopy($source, $target);
+
+ // The source needs to be deletable for moving
+ $sourceNodeFileInfo = $sourceNode->getFileInfo();
+ if (!$sourceNodeFileInfo->isDeletable()) {
+ throw new Forbidden($source . ' cannot be deleted');
+ }
+
+ // The source is not allowed to be the parent of the target
+ if (str_starts_with($source, $target . '/')) {
+ throw new Forbidden($source . ' cannot be moved to it\'s parent');
}
}
@@ -240,8 +257,8 @@ class FilesPlugin extends ServerPlugin {
// adds a 'Content-Disposition: attachment' header in case no disposition
// header has been set before
- if ($this->downloadAttachment &&
- $response->getHeader('Content-Disposition') === null) {
+ if ($this->downloadAttachment
+ && $response->getHeader('Content-Disposition') === null) {
$filename = $node->getName();
if ($this->request->isUserAgent(
[
@@ -256,7 +273,7 @@ class FilesPlugin extends ServerPlugin {
}
}
- if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ if ($node instanceof File) {
//Add OC-Checksum header
$checksum = $node->getChecksum();
if ($checksum !== null && $checksum !== '') {
@@ -276,7 +293,7 @@ class FilesPlugin extends ServerPlugin {
public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
$httpRequest = $this->server->httpRequest;
- if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
+ if ($node instanceof Node) {
/**
* This was disabled, because it made dir listing throw an exception,
* so users were unable to navigate into folders where one subitem
@@ -347,9 +364,32 @@ class FilesPlugin extends ServerPlugin {
$owner = $node->getOwner();
if (!$owner) {
return null;
- } else {
+ }
+
+ // Get current user to see if we're in a public share or not
+ $user = $this->userSession->getUser();
+
+ // If the user is logged in, we can return the display name
+ if ($user !== null) {
+ return $owner->getDisplayName();
+ }
+
+ // Check if the user published their display name
+ try {
+ $ownerAccount = $this->accountManager->getAccount($owner);
+ } catch (NoUserException) {
+ // do not lock process if owner is not local
+ return null;
+ }
+
+ $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
+
+ // Since we are not logged in, we need to have at least the published scope
+ if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) {
return $owner->getDisplayName();
}
+
+ return null;
});
$propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) {
@@ -372,17 +412,27 @@ class FilesPlugin extends ServerPlugin {
return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
});
- $propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest): ?string {
+ $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string {
$user = $this->userSession->getUser();
- if ($user === null) {
- return null;
- }
return $node->getNoteFromShare(
- $user->getUID()
+ $user?->getUID()
);
});
- $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () use ($node) {
+ $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) {
+ $storage = $node->getNode()->getStorage();
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ return match($storage->getShare()->getHideDownload()) {
+ true => 'true',
+ false => 'false',
+ };
+ } else {
+ return null;
+ }
+ });
+
+ $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () {
return $this->config->getSystemValue('data-fingerprint', '');
});
$propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) {
@@ -413,9 +463,14 @@ class FilesPlugin extends ServerPlugin {
$propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) {
return $node->getName();
});
+
+ $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) {
+ return $node->getFileInfo()->getMountPoint()
+ instanceof SharingExternalMount;
+ });
}
- if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ if ($node instanceof File) {
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) {
try {
$directDownloadUrl = $node->getDirectDownload();
@@ -449,10 +504,6 @@ class FilesPlugin extends ServerPlugin {
return $node->getSize();
});
- $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) {
- return $node->getFileInfo()->isEncrypted() ? '1' : '0';
- });
-
$requestProperties = $propFind->getRequestedProperties();
if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true)
@@ -490,8 +541,8 @@ class FilesPlugin extends ServerPlugin {
$ocmPermissions[] = 'read';
}
- if (($ncPermissions & Constants::PERMISSION_CREATE) ||
- ($ncPermissions & Constants::PERMISSION_UPDATE)) {
+ if (($ncPermissions & Constants::PERMISSION_CREATE)
+ || ($ncPermissions & Constants::PERMISSION_UPDATE)) {
$ocmPermissions[] = 'write';
}
@@ -508,7 +559,7 @@ class FilesPlugin extends ServerPlugin {
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
@@ -537,7 +588,7 @@ class FilesPlugin extends ServerPlugin {
if (empty($time)) {
return false;
}
- $node->setCreationTime((int) $time);
+ $node->setCreationTime((int)$time);
return true;
});
@@ -579,7 +630,9 @@ class FilesPlugin extends ServerPlugin {
$propPatch->handle(
$mutation,
function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool {
+ /** @var FilesMetadata $metadata */
$metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true);
+ $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId());
$metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX));
// confirm metadata key is editable via PROPPATCH
@@ -587,6 +640,12 @@ class FilesPlugin extends ServerPlugin {
throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
}
+ if ($value === null) {
+ $metadata->unset($metadataKey);
+ $filesMetadataManager->saveMetadata($metadata);
+ return true;
+ }
+
// If the metadata is unknown, it defaults to string.
try {
$type = $knownMetadata->getType($metadataKey);
@@ -660,8 +719,6 @@ class FilesPlugin extends ServerPlugin {
return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION;
}
-
-
/**
* @param string $filePath
* @param ?\Sabre\DAV\INode $node
@@ -669,25 +726,16 @@ class FilesPlugin extends ServerPlugin {
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) {
- // chunked upload handling
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- [$path, $name] = \Sabre\Uri\split($filePath);
- $info = \OC_FileChunking::decodeName($name);
- if (!empty($info)) {
- $filePath = $path . '/' . $info['name'];
- }
- }
-
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
- if (!$this->server->tree->nodeExists($filePath)) {
- return;
- }
- $node = $this->server->tree->getNodeForPath($filePath);
- if ($node instanceof \OCA\DAV\Connector\Sabre\Node) {
- $fileId = $node->getFileId();
- if (!is_null($fileId)) {
- $this->server->httpResponse->setHeader('OC-FileId', $fileId);
+ try {
+ $node = $this->server->tree->getNodeForPath($filePath);
+ if ($node instanceof Node) {
+ $fileId = $node->getFileId();
+ if (!is_null($fileId)) {
+ $this->server->httpResponse->setHeader('OC-FileId', $fileId);
+ }
}
+ } catch (NotFound) {
}
}
}
diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
index 6a8cb3f0f59..b59d1373af5 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -1,34 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\Files\View;
+use OCA\Circles\Api\v1\Circles;
use OCP\App\IAppManager;
+use OCP\AppFramework\Http;
use OCP\Files\Folder;
use OCP\Files\Node as INode;
use OCP\IGroupManager;
@@ -61,55 +43,8 @@ class FilesReportPlugin extends ServerPlugin {
private $server;
/**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var View
- */
- private $fileView;
-
- /**
- * @var ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var ISystemTagObjectMapper
- */
- private $tagMapper;
-
- /**
- * Manager for private tags
- *
- * @var ITagManager
- */
- private $fileTagger;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var Folder
- */
- private $userFolder;
-
- /**
- * @var IAppManager
- */
- private $appManager;
-
- /**
* @param Tree $tree
- * @param View $view
+ * @param View $fileView
* @param ISystemTagManager $tagManager
* @param ISystemTagObjectMapper $tagMapper
* @param ITagManager $fileTagger manager for private tags
@@ -118,25 +53,20 @@ class FilesReportPlugin extends ServerPlugin {
* @param Folder $userFolder
* @param IAppManager $appManager
*/
- public function __construct(Tree $tree,
- View $view,
- ISystemTagManager $tagManager,
- ISystemTagObjectMapper $tagMapper,
- ITagManager $fileTagger,
- IUserSession $userSession,
- IGroupManager $groupManager,
- Folder $userFolder,
- IAppManager $appManager
+ public function __construct(
+ private Tree $tree,
+ private View $fileView,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ /**
+ * Manager for private tags
+ */
+ private ITagManager $fileTagger,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private Folder $userFolder,
+ private IAppManager $appManager,
) {
- $this->tree = $tree;
- $this->fileView = $view;
- $this->tagManager = $tagManager;
- $this->tagMapper = $tagMapper;
- $this->fileTagger = $fileTagger;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->userFolder = $userFolder;
- $this->appManager = $appManager;
}
/**
@@ -226,7 +156,7 @@ class FilesReportPlugin extends ServerPlugin {
// to user backends. I.e. the final result may return more results than requested.
$resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null);
} catch (TagNotFoundException $e) {
- throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
+ throw new PreconditionFailed('Cannot filter by non-existing tag');
}
$results = [];
@@ -254,7 +184,7 @@ class FilesReportPlugin extends ServerPlugin {
new MultiStatus($responses)
);
- $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setBody($xml);
@@ -375,7 +305,7 @@ class FilesReportPlugin extends ServerPlugin {
if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
return [];
}
- return \OCA\Circles\Api\v1\Circles::getFilesForCircles($circlesIds);
+ return Circles::getFilesForCircles($circlesIds);
}
@@ -383,7 +313,7 @@ class FilesReportPlugin extends ServerPlugin {
* Prepare propfind response for the given nodes
*
* @param string $filesUri $filesUri URI leading to root of the files URI,
- * with a leading slash but no trailing slash
+ * with a leading slash but no trailing slash
* @param string[] $requestedProps requested properties
* @param Node[] nodes nodes for which to fetch and prepare responses
* @return Response[]
@@ -430,7 +360,7 @@ class FilesReportPlugin extends ServerPlugin {
$results = [];
foreach ($fileIds as $fileId) {
- $entry = $folder->getFirstNodeById($fileId);
+ $entry = $folder->getFirstNodeById((int)$fileId);
if ($entry) {
$results[] = $this->wrapNode($entry);
}
@@ -439,7 +369,7 @@ class FilesReportPlugin extends ServerPlugin {
return $results;
}
- protected function wrapNode(\OCP\Files\Node $node): File|Directory {
+ protected function wrapNode(INode $node): File|Directory {
if ($node instanceof \OCP\Files\File) {
return new File($this->fileView, $node);
} else {
diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php
index 6305b0ec138..6640771dc31 100644
--- a/apps/dav/lib/Connector/Sabre/LockPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jaakko Salo <jaakkos@gmail.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -61,7 +42,7 @@ class LockPlugin extends ServerPlugin {
public function getLock(RequestInterface $request) {
// we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
// so instead we limit ourselves to the PUT method manually
- if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ if ($request->getMethod() !== 'PUT') {
return;
}
try {
@@ -84,7 +65,7 @@ class LockPlugin extends ServerPlugin {
if ($this->isLocked === false) {
return;
}
- if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ if ($request->getMethod() !== 'PUT') {
return;
}
try {
diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
index 1fc02320805..d5ab7f09dfa 100644
--- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
+++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php
@@ -1,29 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Valdnet <47037905+Valdnet@users.noreply.github.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -36,10 +16,7 @@ use Sabre\DAV\ServerPlugin;
class MaintenancePlugin extends ServerPlugin {
- /** @var IConfig */
- private $config;
-
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
/**
@@ -52,8 +29,10 @@ class MaintenancePlugin extends ServerPlugin {
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config, IL10N $l10n) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ IL10N $l10n,
+ ) {
$this->l10n = \OC::$server->getL10N('dav');
}
diff --git a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
index 6700b1eb81b..e18ef58149a 100644
--- a/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
+++ b/apps/dav/lib/Connector/Sabre/MtimeSanitizer.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php
index 2b77dc08e8d..505e6b5eda4 100644
--- a/apps/dav/lib/Connector/Sabre/Node.php
+++ b/apps/dav/lib/Connector/Sabre/Node.php
@@ -1,37 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Klaas Freitag <freitag@owncloud.com>
- * @author Markus Goetz <markus@woboq.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -40,20 +12,20 @@ use OC\Files\Node\File;
use OC\Files\Node\Folder;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
+use OCP\Constants;
use OCP\Files\DavUtil;
use OCP\Files\FileInfo;
+use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\ISharedStorage;
use OCP\Files\StorageNotAvailableException;
+use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
abstract class Node implements \Sabre\DAV\INode {
/**
- * @var View
- */
- protected $fileView;
-
- /**
* The path to the current node
*
* @var string
@@ -79,21 +51,24 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Sets up the node, expects a full path name
*/
- public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null) {
- $this->fileView = $view;
+ public function __construct(
+ protected View $fileView,
+ FileInfo $info,
+ ?IManager $shareManager = null,
+ ) {
$this->path = $this->fileView->getRelativePath($info->getPath());
$this->info = $info;
if ($shareManager) {
$this->shareManager = $shareManager;
} else {
- $this->shareManager = \OC::$server->getShareManager();
+ $this->shareManager = Server::get(\OCP\Share\IManager::class);
}
if ($info instanceof Folder || $info instanceof File) {
$this->node = $info;
} else {
// The Node API assumes that the view passed doesn't have a fake root
- $rootView = \OC::$server->get(View::class);
- $root = \OC::$server->get(IRootFolder::class);
+ $rootView = Server::get(View::class);
+ $root = Server::get(IRootFolder::class);
if ($info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->fileView->getAbsolutePath($this->path), $info);
} else {
@@ -105,11 +80,11 @@ abstract class Node implements \Sabre\DAV\INode {
protected function refreshInfo(): void {
$info = $this->fileView->getFileInfo($this->path);
if ($info === false) {
- throw new \Sabre\DAV\Exception('Failed to get fileinfo for '. $this->path);
+ throw new \Sabre\DAV\Exception('Failed to get fileinfo for ' . $this->path);
}
$this->info = $info;
- $root = \OC::$server->get(IRootFolder::class);
- $rootView = \OC::$server->get(View::class);
+ $root = Server::get(IRootFolder::class);
+ $rootView = Server::get(View::class);
if ($this->info->getType() === FileInfo::TYPE_FOLDER) {
$this->node = new Folder($root, $rootView, $this->path, $this->info);
} else {
@@ -143,21 +118,21 @@ abstract class Node implements \Sabre\DAV\INode {
* @throws \Sabre\DAV\Exception\Forbidden
*/
public function setName($name) {
- // rename is only allowed if the update privilege is granted
- if (!($this->info->isUpdateable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
+ // rename is only allowed if the delete privilege is granted
+ // (basically rename is a copy with delete of the original node)
+ if (!($this->info->isDeletable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
throw new \Sabre\DAV\Exception\Forbidden();
}
[$parentPath,] = \Sabre\Uri\split($this->path);
[, $newName] = \Sabre\Uri\split($name);
+ $newPath = $parentPath . '/' . $newName;
// verify path of the target
- $this->verifyPath();
-
- $newPath = $parentPath . '/' . $newName;
+ $this->verifyPath($newPath);
if (!$this->fileView->rename($this->path, $newPath)) {
- throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath);
+ throw new \Sabre\DAV\Exception('Failed to rename ' . $this->path . ' to ' . $newPath);
}
$this->path = $newPath;
@@ -289,8 +264,8 @@ abstract class Node implements \Sabre\DAV\INode {
$storage = null;
}
- if ($storage && $storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
+ if ($storage && $storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
$permissions = (int)$storage->getShare()->getPermissions();
} else {
$permissions = $this->info->getPermissions();
@@ -308,15 +283,15 @@ abstract class Node implements \Sabre\DAV\INode {
}
if (!$mountpoint->getOption('readonly', false) && $mountpointpath === $this->info->getPath()) {
- $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
+ $permissions |= Constants::PERMISSION_DELETE | Constants::PERMISSION_UPDATE;
}
}
/*
* Files can't have create or delete permissions
*/
- if ($this->info->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
- $permissions &= ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE);
+ if ($this->info->getType() === FileInfo::TYPE_FILE) {
+ $permissions &= ~(Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE);
}
return $permissions;
@@ -326,16 +301,15 @@ abstract class Node implements \Sabre\DAV\INode {
* @return array
*/
public function getShareAttributes(): array {
- $attributes = [];
-
try {
- $storage = $this->info->getStorage();
- } catch (StorageNotAvailableException $e) {
- $storage = null;
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException $e) {
+ return [];
}
- if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
+ $attributes = [];
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
$attributes = $storage->getShare()->getAttributes();
if ($attributes === null) {
return [];
@@ -347,29 +321,24 @@ abstract class Node implements \Sabre\DAV\INode {
return $attributes;
}
- /**
- * @param string $user
- * @return string
- */
- public function getNoteFromShare($user) {
- if ($user === null) {
- return '';
+ public function getNoteFromShare(?string $user): ?string {
+ try {
+ $storage = $this->node->getStorage();
+ } catch (NotFoundException) {
+ return null;
}
- // Retrieve note from the share object already loaded into
- // memory, to avoid additional database queries.
- $storage = $this->getNode()->getStorage();
- if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
- return '';
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ /** @var ISharedStorage $storage */
+ $share = $storage->getShare();
+ if ($user === $share->getShareOwner()) {
+ // Note is only for recipient not the owner
+ return null;
+ }
+ return $share->getNote();
}
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
- $share = $storage->getShare();
- $note = $share->getNote();
- if ($share->getShareOwner() !== $user) {
- return $note;
- }
- return '';
+ return null;
}
/**
@@ -383,11 +352,14 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->info->getOwner();
}
- protected function verifyPath() {
+ protected function verifyPath(?string $path = null): void {
try {
- $fileName = basename($this->info->getPath());
- $this->fileView->verifyPath($this->path, $fileName);
- } catch (\OCP\Files\InvalidPathException $ex) {
+ $path = $path ?? $this->info->getPath();
+ $this->fileView->verifyPath(
+ dirname($path),
+ basename($path),
+ );
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
@@ -421,7 +393,7 @@ abstract class Node implements \Sabre\DAV\INode {
return $this->node;
}
- protected function sanitizeMtime($mtimeFromRequest) {
+ protected function sanitizeMtime(string $mtimeFromRequest): int {
return MtimeSanitizer::sanitizeMtime($mtimeFromRequest);
}
}
diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php
index c129371e376..bfbdfb33db0 100644
--- a/apps/dav/lib/Connector/Sabre/ObjectTree.php
+++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php
@@ -1,39 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\Files\FileInfo;
use OC\Files\Storage\FailedStorage;
+use OC\Files\Storage\Storage;
+use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\ForbiddenException;
+use OCP\Files\InvalidPathException;
+use OCP\Files\Mount\IMountManager;
use OCP\Files\StorageInvalidException;
use OCP\Files\StorageNotAvailableException;
use OCP\Lock\LockedException;
@@ -41,12 +24,12 @@ use OCP\Lock\LockedException;
class ObjectTree extends CachingTree {
/**
- * @var \OC\Files\View
+ * @var View
*/
protected $fileView;
/**
- * @var \OCP\Files\Mount\IMountManager
+ * @var IMountManager
*/
protected $mountManager;
@@ -58,45 +41,16 @@ class ObjectTree extends CachingTree {
/**
* @param \Sabre\DAV\INode $rootNode
- * @param \OC\Files\View $view
- * @param \OCP\Files\Mount\IMountManager $mountManager
+ * @param View $view
+ * @param IMountManager $mountManager
*/
- public function init(\Sabre\DAV\INode $rootNode, \OC\Files\View $view, \OCP\Files\Mount\IMountManager $mountManager) {
+ public function init(\Sabre\DAV\INode $rootNode, View $view, IMountManager $mountManager) {
$this->rootNode = $rootNode;
$this->fileView = $view;
$this->mountManager = $mountManager;
}
/**
- * If the given path is a chunked file name, converts it
- * to the real file name. Only applies if the OC-CHUNKED header
- * is present.
- *
- * @param string $path chunk file path to convert
- *
- * @return string path to real file
- */
- private function resolveChunkFile($path) {
- if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
- // resolve to real file name to find the proper node
- [$dir, $name] = \Sabre\Uri\split($path);
- if ($dir === '/' || $dir === '.') {
- $dir = '';
- }
-
- $info = \OC_FileChunking::decodeName($name);
- // only replace path if it was really the chunked file
- if (isset($info['transferid'])) {
- // getNodePath is called for multiple nodes within a chunk
- // upload call
- $path = $dir . '/' . $info['name'];
- $path = ltrim($path, '/');
- }
- }
- return $path;
- }
-
- /**
* Returns the INode object for the requested path
*
* @param string $path
@@ -120,7 +74,7 @@ class ObjectTree extends CachingTree {
if ($path) {
try {
$this->fileView->verifyPath($path, basename($path));
- } catch (\OCP\Files\InvalidPathException $ex) {
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
}
@@ -138,7 +92,7 @@ class ObjectTree extends CachingTree {
$internalPath = $mount->getInternalPath($absPath);
if ($storage && $storage->file_exists($internalPath)) {
/**
- * @var \OC\Files\Storage\Storage $storage
+ * @var Storage $storage
*/
// get data directly
$data = $storage->getMetaData($internalPath);
@@ -147,9 +101,6 @@ class ObjectTree extends CachingTree {
$info = null;
}
} else {
- // resolve chunk file name to real name, if applicable
- $path = $this->resolveChunkFile($path);
-
// read from cache
try {
$info = $this->fileView->getFileInfo($path);
@@ -173,9 +124,9 @@ class ObjectTree extends CachingTree {
}
if ($info->getType() === 'dir') {
- $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this);
+ $node = new Directory($this->fileView, $info, $this);
} else {
- $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
+ $node = new File($this->fileView, $info);
}
$this->cache[$path] = $node;
@@ -222,7 +173,7 @@ class ObjectTree extends CachingTree {
[$destinationDir, $destinationName] = \Sabre\Uri\split($destinationPath);
try {
$this->fileView->verifyPath($destinationDir, $destinationName);
- } catch (\OCP\Files\InvalidPathException $ex) {
+ } catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
}
diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php
index c6f9fe3affc..d6ea9fd887d 100644
--- a/apps/dav/lib/Connector/Sabre/Principal.php
+++ b/apps/dav/lib/Connector/Sabre/Principal.php
@@ -1,44 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
use OC\KnownUser\KnownUserService;
+use OCA\Circles\Api\v1\Circles;
use OCA\Circles\Exceptions\CircleNotFoundException;
+use OCA\Circles\Model\Circle;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Traits\PrincipalProxyTrait;
use OCP\Accounts\IAccountManager;
@@ -61,24 +33,6 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class Principal implements BackendInterface {
- /** @var IUserManager */
- private $userManager;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IAccountManager */
- private $accountManager;
-
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IAppManager */
- private $appManager;
-
/** @var string */
private $principalPrefix;
@@ -88,40 +42,25 @@ class Principal implements BackendInterface {
/** @var bool */
private $hasCircles;
- /** @var ProxyMapper */
- private $proxyMapper;
-
/** @var KnownUserService */
private $knownUserService;
- /** @var IConfig */
- private $config;
- /** @var IFactory */
- private $languageFactory;
-
- public function __construct(IUserManager $userManager,
- IGroupManager $groupManager,
- IAccountManager $accountManager,
- IShareManager $shareManager,
- IUserSession $userSession,
- IAppManager $appManager,
- ProxyMapper $proxyMapper,
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private IAccountManager $accountManager,
+ private IShareManager $shareManager,
+ private IUserSession $userSession,
+ private IAppManager $appManager,
+ private ProxyMapper $proxyMapper,
KnownUserService $knownUserService,
- IConfig $config,
- IFactory $languageFactory,
- string $principalPrefix = 'principals/users/') {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->accountManager = $accountManager;
- $this->shareManager = $shareManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ private IConfig $config,
+ private IFactory $languageFactory,
+ string $principalPrefix = 'principals/users/',
+ ) {
$this->principalPrefix = trim($principalPrefix, '/');
$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
- $this->proxyMapper = $proxyMapper;
$this->knownUserService = $knownUserService;
- $this->config = $config;
- $this->languageFactory = $languageFactory;
}
use PrincipalProxyTrait {
@@ -211,7 +150,12 @@ class Principal implements BackendInterface {
} elseif ($prefix === 'principals/system') {
return [
'uri' => 'principals/system/' . $name,
- '{DAV:}displayname' => $this->languageFactory->get('dav')->t("Accounts"),
+ '{DAV:}displayname' => $this->languageFactory->get('dav')->t('Accounts'),
+ ];
+ } elseif ($prefix === 'principals/shares') {
+ return [
+ 'uri' => 'principals/shares/' . $name,
+ '{DAV:}displayname' => $name,
];
}
return null;
@@ -242,6 +186,9 @@ class Principal implements BackendInterface {
if ($this->hasGroups || $needGroups) {
$userGroups = $this->groupManager->getUserGroups($user);
foreach ($userGroups as $userGroup) {
+ if ($userGroup->hideFromCollaboration()) {
+ continue;
+ }
$groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
}
}
@@ -561,7 +508,7 @@ class Principal implements BackendInterface {
}
try {
- $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
+ $circle = Circles::detailsCircle($circleUniqueId, true);
} catch (QueryException $ex) {
return null;
} catch (CircleNotFoundException $ex) {
@@ -586,7 +533,7 @@ class Principal implements BackendInterface {
* @param string $principal
* @return array
* @throws Exception
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
* @suppress PhanUndeclaredClassMethod
*/
public function getCircleMembership($principal):array {
@@ -601,10 +548,10 @@ class Principal implements BackendInterface {
throw new Exception('Principal not found');
}
- $circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
+ $circles = Circles::joinedCircles($name, true);
$circles = array_map(function ($circle) {
- /** @var \OCA\Circles\Model\Circle $circle */
+ /** @var Circle $circle */
return 'principals/circles/' . urlencode($circle->getSingleId());
}, $circles);
diff --git a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php
new file mode 100644
index 00000000000..130d4562146
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types = 1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use Sabre\DAV\Server as SabreServer;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * This plugin runs after requests and logs an error if a plugin is detected
+ * to be doing too many SQL requests.
+ */
+class PropFindMonitorPlugin extends ServerPlugin {
+
+ /**
+ * A Plugin can scan up to this amount of nodes without an error being
+ * reported.
+ */
+ public const THRESHOLD_NODES = 50;
+
+ /**
+ * A plugin can use up to this amount of queries per node.
+ */
+ public const THRESHOLD_QUERY_FACTOR = 1;
+
+ private SabreServer $server;
+
+ public function initialize(SabreServer $server): void {
+ $this->server = $server;
+ $this->server->on('afterResponse', [$this, 'afterResponse']);
+ }
+
+ public function afterResponse(
+ RequestInterface $request,
+ ResponseInterface $response): void {
+ if (!$this->server instanceof Server) {
+ return;
+ }
+
+ $pluginQueries = $this->server->getPluginQueries();
+ if (empty($pluginQueries)) {
+ return;
+ }
+ $maxDepth = max(0, ...array_keys($pluginQueries));
+ // entries at the top are usually not interesting
+ unset($pluginQueries[$maxDepth]);
+
+ $logger = $this->server->getLogger();
+ foreach ($pluginQueries as $depth => $propFinds) {
+ foreach ($propFinds as $pluginName => $propFind) {
+ [
+ 'queries' => $queries,
+ 'nodes' => $nodes
+ ] = $propFind;
+ if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES
+ || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) {
+ continue;
+ }
+ $logger->error(
+ '{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [
+ 'name' => $pluginName,
+ 'scans' => $nodes,
+ 'count' => $queries,
+ 'depth' => $depth,
+ 'maxDepth' => $maxDepth,
+ ]
+ );
+ }
+ }
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
index 0c274d807a0..15daf1f34b6 100644
--- a/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/PropfindCompressionPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
diff --git a/apps/dav/lib/Connector/Sabre/PublicAuth.php b/apps/dav/lib/Connector/Sabre/PublicAuth.php
index d5b3d41e1ef..2ca1c25e2f6 100644
--- a/apps/dav/lib/Connector/Sabre/PublicAuth.php
+++ b/apps/dav/lib/Connector/Sabre/PublicAuth.php
@@ -2,40 +2,21 @@
declare(strict_types=1);
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OCP\Defaults;
use OCP\IRequest;
use OCP\ISession;
+use OCP\IURLGenerator;
use OCP\Security\Bruteforce\IThrottler;
+use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -43,6 +24,7 @@ use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Backend\AbstractBasic;
use Sabre\DAV\Exception\NotAuthenticated;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
@@ -58,40 +40,33 @@ class PublicAuth extends AbstractBasic {
public const DAV_AUTHENTICATED = 'public_link_authenticated';
private ?IShare $share = null;
- private IManager $shareManager;
- private ISession $session;
- private IRequest $request;
- private IThrottler $throttler;
- private LoggerInterface $logger;
-
- public function __construct(IRequest $request,
- IManager $shareManager,
- ISession $session,
- IThrottler $throttler,
- LoggerInterface $logger) {
- $this->request = $request;
- $this->shareManager = $shareManager;
- $this->session = $session;
- $this->throttler = $throttler;
- $this->logger = $logger;
+ public function __construct(
+ private IRequest $request,
+ private IManager $shareManager,
+ private ISession $session,
+ private IThrottler $throttler,
+ private LoggerInterface $logger,
+ private IURLGenerator $urlGenerator,
+ ) {
// setup realm
- $defaults = new \OCP\Defaults();
+ $defaults = new Defaults();
$this->realm = $defaults->getName();
}
/**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- *
- * @return array
* @throws NotAuthenticated
+ * @throws MaxDelayReached
* @throws ServiceUnavailable
*/
public function check(RequestInterface $request, ResponseInterface $response): array {
try {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
+ if (count($_COOKIE) > 0 && !$this->request->passesStrictCookieCheck() && $this->getShare()->getPassword() !== null) {
+ throw new PreconditionFailed('Strict cookie check failed');
+ }
+
$auth = new HTTP\Auth\Basic(
$this->realm,
$request,
@@ -105,7 +80,17 @@ class PublicAuth extends AbstractBasic {
}
return $this->checkToken();
- } catch (NotAuthenticated $e) {
+ } catch (NotAuthenticated|MaxDelayReached $e) {
+ $this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
+ throw $e;
+ } catch (PreconditionFailed $e) {
+ $response->setHeader(
+ 'Location',
+ $this->urlGenerator->linkToRoute(
+ 'files_sharing.share.showShare',
+ [ 'token' => $this->getToken() ],
+ ),
+ );
throw $e;
} catch (\Exception $e) {
$class = get_class($e);
@@ -117,14 +102,13 @@ class PublicAuth extends AbstractBasic {
/**
* Extract token from request url
- * @return string
* @throws NotFound
*/
private function getToken(): string {
$path = $this->request->getPathInfo() ?: '';
// ['', 'dav', 'files', 'token']
$splittedPath = explode('/', $path);
-
+
if (count($splittedPath) < 4 || $splittedPath[3] === '') {
throw new NotFound();
}
@@ -134,7 +118,7 @@ class PublicAuth extends AbstractBasic {
/**
* Check token validity
- * @return array
+ *
* @throws NotFound
* @throws NotAuthenticated
*/
@@ -182,15 +166,13 @@ class PublicAuth extends AbstractBasic {
protected function validateUserPass($username, $password) {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
- $token = $this->getToken();
try {
- $share = $this->shareManager->getShareByToken($token);
+ $share = $this->getShare();
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
- $this->share = $share;
\OC_User::setIncognitoMode(true);
// check if the share is password protected
@@ -206,7 +188,7 @@ class PublicAuth extends AbstractBasic {
}
return true;
}
-
+
if ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)
&& $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {
return true;
@@ -233,7 +215,13 @@ class PublicAuth extends AbstractBasic {
}
public function getShare(): IShare {
- assert($this->share !== null);
+ $token = $this->getToken();
+
+ if ($this->share === null) {
+ $share = $this->shareManager->getShareByToken($token);
+ $this->share = $share;
+ }
+
return $this->share;
}
}
diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
index b37325430e7..bbb378edc9b 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -1,41 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
- * @copyright Copyright (C) 2012 entreCables S.L. All rights reserved.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Felix Moeller <mail@felixmoeller.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author scambra <sergio@entrecables.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2012 entreCables S.L. All rights reserved
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Files\View;
use OCA\DAV\Upload\FutureFile;
use OCA\DAV\Upload\UploadFolder;
use OCP\Files\StorageNotAvailableException;
use Sabre\DAV\Exception\InsufficientStorage;
use Sabre\DAV\Exception\ServiceUnavailable;
use Sabre\DAV\INode;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
/**
* This plugin check user quota and deny creating files when they exceeds the quota.
@@ -45,9 +26,6 @@ use Sabre\DAV\INode;
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
- /** @var \OC\Files\View */
- private $view;
-
/**
* Reference to main server object
*
@@ -56,10 +34,11 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
private $server;
/**
- * @param \OC\Files\View $view
+ * @param View $view
*/
- public function __construct($view) {
- $this->view = $view;
+ public function __construct(
+ private $view,
+ ) {
}
/**
@@ -78,6 +57,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
$server->on('beforeCreateFile', [$this, 'beforeCreateFile'], 10);
+ $server->on('method:MKCOL', [$this, 'onCreateCollection'], 30);
$server->on('beforeMove', [$this, 'beforeMove'], 10);
$server->on('beforeCopy', [$this, 'beforeCopy'], 10);
}
@@ -112,6 +92,31 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
+ * Check quota before creating directory
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool
+ * @throws InsufficientStorage
+ * @throws \Sabre\DAV\Exception\Forbidden
+ */
+ public function onCreateCollection(RequestInterface $request, ResponseInterface $response): bool {
+ try {
+ $destinationPath = $this->server->calculateUri($request->getUrl());
+ $quotaPath = $this->getPathForDestination($destinationPath);
+ } catch (\Exception $e) {
+ return true;
+ }
+ if ($quotaPath) {
+ // MKCOL does not have a Content-Length header, so we can use
+ // a fixed value for the quota check.
+ return $this->checkQuota($quotaPath, 4096, true);
+ }
+
+ return true;
+ }
+
+ /**
* Check quota before writing content
*
* @param string $uri target file URI
@@ -197,7 +202,7 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
* @throws InsufficientStorage
* @return bool
*/
- public function checkQuota(string $path, $length = null) {
+ public function checkQuota(string $path, $length = null, $isDir = false) {
if ($length === null) {
$length = $this->getLength();
}
@@ -209,26 +214,15 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
$req = $this->server->httpRequest;
- // If LEGACY chunked upload
- if ($req->getHeader('OC-Chunked')) {
- $info = \OC_FileChunking::decodeName($newName);
- $chunkHandler = $this->getFileChunking($info);
- // subtract the already uploaded size to see whether
- // there is still enough space for the remaining chunks
- $length -= $chunkHandler->getCurrentSize();
- // use target file name for free space check in case of shared files
- $path = rtrim($parentPath, '/') . '/' . $info['name'];
- }
-
// Strip any duplicate slashes
$path = str_replace('//', '/', $path);
$freeSpace = $this->getFreeSpace($path);
if ($freeSpace >= 0 && $length > $freeSpace) {
- // If LEGACY chunked upload, clean up
- if (isset($chunkHandler)) {
- $chunkHandler->cleanup();
+ if ($isDir) {
+ throw new InsufficientStorage("Insufficient space in $path. $freeSpace available. Cannot create directory");
}
+
throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
}
}
@@ -236,11 +230,6 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
return true;
}
- public function getFileChunking($info) {
- // FIXME: need a factory for better mocking support
- return new \OC_FileChunking($info);
- }
-
public function getLength() {
$req = $this->server->httpRequest;
$length = $req->getHeader('X-Expected-Entity-Length');
diff --git a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
index df82db59f1d..5484bab9237 100644
--- a/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/RequestIdHeaderPlugin.php
@@ -2,23 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -28,11 +13,9 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class RequestIdHeaderPlugin extends \Sabre\DAV\ServerPlugin {
- /** @var IRequest */
- private $request;
-
- public function __construct(IRequest $request) {
- $this->request = $request;
+ public function __construct(
+ private IRequest $request,
+ ) {
}
public function initialize(\Sabre\DAV\Server $server) {
diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php
index 6cf6fa954c8..dda9c29b763 100644
--- a/apps/dav/lib/Connector/Sabre/Server.php
+++ b/apps/dav/lib/Connector/Sabre/Server.php
@@ -1,30 +1,20 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author scolebrook <scolebrook@mac.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\DB\Connection;
+use Override;
+use Sabre\DAV\Exception;
+use Sabre\DAV\INode;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Version;
+use TypeError;
+
/**
* Class \OCA\DAV\Connector\Sabre\Server
*
@@ -36,6 +26,14 @@ class Server extends \Sabre\DAV\Server {
/** @var CachingTree $tree */
/**
+ * Tracks queries done by plugins.
+ * @var array<int, array<string, array{nodes:int, queries:int}>>
+ */
+ private array $pluginQueries = [];
+
+ public bool $debugEnabled = false;
+
+ /**
* @see \Sabre\DAV\Server
*/
public function __construct($treeOrNode = null) {
@@ -43,4 +41,190 @@ class Server extends \Sabre\DAV\Server {
self::$exposeVersion = false;
$this->enablePropfindDepthInfinity = true;
}
+
+ #[Override]
+ public function once(
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ $this->debugEnabled ? $this->monitorPropfindQueries(
+ parent::once(...),
+ ...func_get_args(),
+ ) : parent::once(...func_get_args());
+ }
+
+ #[Override]
+ public function on(
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ $this->debugEnabled ? $this->monitorPropfindQueries(
+ parent::on(...),
+ ...func_get_args(),
+ ) : parent::on(...func_get_args());
+ }
+
+ /**
+ * Wraps the handler $callBack into a query-monitoring function and calls
+ * $parentFn to register it.
+ */
+ private function monitorPropfindQueries(
+ callable $parentFn,
+ string $eventName,
+ callable $callBack,
+ int $priority = 100,
+ ): void {
+ if ($eventName !== 'propFind') {
+ $parentFn($eventName, $callBack, $priority);
+ return;
+ }
+
+ $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown';
+ $callback = $this->getMonitoredCallback($callBack, $pluginName);
+
+ $parentFn($eventName, $callback, $priority);
+ }
+
+ /**
+ * Returns a callable that wraps $callBack with code that monitors and
+ * records queries per plugin.
+ */
+ private function getMonitoredCallback(
+ callable $callBack,
+ string $pluginName,
+ ): callable {
+ return function (PropFind $propFind, INode $node) use (
+ $callBack,
+ $pluginName,
+ ) {
+ $connection = \OCP\Server::get(Connection::class);
+ $queriesBefore = $connection->getStats()['executed'];
+ $result = $callBack($propFind, $node);
+ $queriesAfter = $connection->getStats()['executed'];
+ $this->trackPluginQueries(
+ $pluginName,
+ $queriesAfter - $queriesBefore,
+ $propFind->getDepth()
+ );
+
+ return $result;
+ };
+ }
+
+ /**
+ * Tracks the queries executed by a specific plugin.
+ */
+ private function trackPluginQueries(
+ string $pluginName,
+ int $queriesExecuted,
+ int $depth,
+ ): void {
+ // report only nodes which cause queries to the DB
+ if ($queriesExecuted === 0) {
+ return;
+ }
+
+ $this->pluginQueries[$depth][$pluginName]['nodes']
+ = ($this->pluginQueries[$depth][$pluginName]['nodes'] ?? 0) + 1;
+
+ $this->pluginQueries[$depth][$pluginName]['queries']
+ = ($this->pluginQueries[$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted;
+ }
+
+ /**
+ *
+ * @return void
+ */
+ public function start() {
+ try {
+ // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
+ // origin, we must make sure we send back HTTP/1.0 if this was
+ // requested.
+ // This is mainly because nginx doesn't support Chunked Transfer
+ // Encoding, and this forces the webserver SabreDAV is running on,
+ // to buffer entire responses to calculate Content-Length.
+ $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
+
+ // Setting the base url
+ $this->httpRequest->setBaseUrl($this->getBaseUri());
+ $this->invokeMethod($this->httpRequest, $this->httpResponse);
+ } catch (\Throwable $e) {
+ try {
+ $this->emit('exception', [$e]);
+ } catch (\Exception) {
+ }
+
+ if ($e instanceof TypeError) {
+ /*
+ * The TypeError includes the file path where the error occurred,
+ * potentially revealing the installation directory.
+ */
+ $e = new TypeError('A type error occurred. For more details, please refer to the logs, which provide additional context about the type error.');
+ }
+
+ $DOM = new \DOMDocument('1.0', 'utf-8');
+ $DOM->formatOutput = true;
+
+ $error = $DOM->createElementNS('DAV:', 'd:error');
+ $error->setAttribute('xmlns:s', self::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ $h = function ($v) {
+ return htmlspecialchars((string)$v, ENT_NOQUOTES, 'UTF-8');
+ };
+
+ if (self::$exposeVersion) {
+ $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
+ }
+
+ $error->appendChild($DOM->createElement('s:exception', $h(get_class($e))));
+ $error->appendChild($DOM->createElement('s:message', $h($e->getMessage())));
+ if ($this->debugExceptions) {
+ $error->appendChild($DOM->createElement('s:file', $h($e->getFile())));
+ $error->appendChild($DOM->createElement('s:line', $h($e->getLine())));
+ $error->appendChild($DOM->createElement('s:code', $h($e->getCode())));
+ $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString())));
+ }
+
+ if ($this->debugExceptions) {
+ $previous = $e;
+ while ($previous = $previous->getPrevious()) {
+ $xPrevious = $DOM->createElement('s:previous-exception');
+ $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous))));
+ $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage())));
+ $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile())));
+ $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine())));
+ $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode())));
+ $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString())));
+ $error->appendChild($xPrevious);
+ }
+ }
+
+ if ($e instanceof Exception) {
+ $httpCode = $e->getHTTPCode();
+ $e->serialize($this, $error);
+ $headers = $e->getHTTPHeaders($this);
+ } else {
+ $httpCode = 500;
+ $headers = [];
+ }
+ $headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ $this->httpResponse->setStatus($httpCode);
+ $this->httpResponse->setHeaders($headers);
+ $this->httpResponse->setBody($DOM->saveXML());
+ $this->sapi->sendResponse($this->httpResponse);
+ }
+ }
+
+ /**
+ * Returns queries executed by registered plugins.
+ *
+ * @return array<int, array<string, array{nodes:int, queries:int}>>
+ */
+ public function getPluginQueries(): array {
+ return $this->pluginQueries;
+ }
}
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php
index 758951c42ff..a6a27057177 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -1,114 +1,111 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Files\View;
+use OC\KnownUser\KnownUserService;
use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
+use OCA\DAV\Files\Sharing\RootCollection;
+use OCA\DAV\Upload\CleanupService;
+use OCA\Theming\ThemingDefaults;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\IConfig;
use OCP\IDBConnection;
+use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ITagManager;
+use OCP\IUserManager;
use OCP\IUserSession;
use OCP\SabrePluginEvent;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Auth\Plugin;
+use Sabre\DAV\SimpleCollection;
class ServerFactory {
- private IConfig $config;
- private LoggerInterface $logger;
- private IDBConnection $databaseConnection;
- private IUserSession $userSession;
- private IMountManager $mountManager;
- private ITagManager $tagManager;
- private IRequest $request;
- private IPreview $previewManager;
- private IEventDispatcher $eventDispatcher;
- private IL10N $l10n;
public function __construct(
- IConfig $config,
- LoggerInterface $logger,
- IDBConnection $databaseConnection,
- IUserSession $userSession,
- IMountManager $mountManager,
- ITagManager $tagManager,
- IRequest $request,
- IPreview $previewManager,
- IEventDispatcher $eventDispatcher,
- IL10N $l10n
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private IDBConnection $databaseConnection,
+ private IUserSession $userSession,
+ private IMountManager $mountManager,
+ private ITagManager $tagManager,
+ private IRequest $request,
+ private IPreview $previewManager,
+ private IEventDispatcher $eventDispatcher,
+ private IL10N $l10n,
) {
- $this->config = $config;
- $this->logger = $logger;
- $this->databaseConnection = $databaseConnection;
- $this->userSession = $userSession;
- $this->mountManager = $mountManager;
- $this->tagManager = $tagManager;
- $this->request = $request;
- $this->previewManager = $previewManager;
- $this->eventDispatcher = $eventDispatcher;
- $this->l10n = $l10n;
}
/**
* @param callable $viewCallBack callback that should return the view for the dav endpoint
*/
- public function createServer(string $baseUri,
+ public function createServer(
+ bool $isPublicShare,
+ string $baseUri,
string $requestUri,
Plugin $authPlugin,
- callable $viewCallBack): Server {
+ callable $viewCallBack,
+ ): Server {
+ $debugEnabled = $this->config->getSystemValue('debug', false);
// Fire up server
- $objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
- $server = new \OCA\DAV\Connector\Sabre\Server($objectTree);
+ if ($isPublicShare) {
+ $rootCollection = new SimpleCollection('root');
+ $tree = new CachingTree($rootCollection);
+ } else {
+ $rootCollection = null;
+ $tree = new ObjectTree();
+ }
+ $server = new Server($tree);
// Set URL explicitly due to reverse-proxy situations
$server->httpRequest->setUrl($requestUri);
$server->setBaseUri($baseUri);
// Load plugins
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin());
+ $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n));
+ $server->addPlugin(new BlockLegacyClientPlugin(
+ $this->config,
+ \OCP\Server::get(ThemingDefaults::class),
+ ));
+ $server->addPlugin(new AnonymousOptionsPlugin());
$server->addPlugin($authPlugin);
+ if ($debugEnabled) {
+ $server->debugEnabled = $debugEnabled;
+ $server->addPlugin(new PropFindMonitorPlugin());
+ }
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
+ $server->addPlugin(new DummyGetResponsePlugin());
+ $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger));
+ $server->addPlugin(new LockPlugin());
- $server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class)));
+ $server->addPlugin(new RequestIdHeaderPlugin($this->request));
+
+ $server->addPlugin(new ZipFolderPlugin(
+ $tree,
+ $this->logger,
+ $this->eventDispatcher,
+ ));
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
// we do not provide locking we emulate it using a fake locking plugin.
@@ -117,7 +114,7 @@ class ServerFactory {
'/OneNote/',
'/Microsoft-WebDAV-MiniRedir/',
])) {
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\FakeLockerPlugin());
+ $server->addPlugin(new FakeLockerPlugin());
}
if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) {
@@ -125,11 +122,12 @@ class ServerFactory {
}
// wait with registering these until auth is handled and the filesystem is setup
- $server->on('beforeMethod:*', function () use ($server, $objectTree, $viewCallBack) {
+ $server->on('beforeMethod:*', function () use ($server, $tree,
+ $viewCallBack, $isPublicShare, $rootCollection, $debugEnabled): void {
// ensure the skeleton is copied
$userFolder = \OC::$server->getUserFolder();
- /** @var \OC\Files\View $view */
+ /** @var View $view */
$view = $viewCallBack($server);
if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) {
$rootInfo = $userFolder;
@@ -139,25 +137,61 @@ class ServerFactory {
// Create Nextcloud Dir
if ($rootInfo->getType() === 'dir') {
- $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree);
+ $root = new Directory($view, $rootInfo, $tree);
+ } else {
+ $root = new File($view, $rootInfo);
+ }
+
+ if ($isPublicShare) {
+ $userPrincipalBackend = new Principal(
+ \OCP\Server::get(IUserManager::class),
+ \OCP\Server::get(IGroupManager::class),
+ \OCP\Server::get(IAccountManager::class),
+ \OCP\Server::get(\OCP\Share\IManager::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IAppManager::class),
+ \OCP\Server::get(ProxyMapper::class),
+ \OCP\Server::get(KnownUserService::class),
+ \OCP\Server::get(IConfig::class),
+ \OC::$server->getL10NFactory(),
+ );
+
+ // Mount the share collection at /public.php/dav/shares/<share token>
+ $rootCollection->addChild(new RootCollection(
+ $root,
+ $userPrincipalBackend,
+ 'principals/shares',
+ ));
+
+ // Mount the upload collection at /public.php/dav/uploads/<share token>
+ $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection(
+ $userPrincipalBackend,
+ 'principals/shares',
+ \OCP\Server::get(CleanupService::class),
+ \OCP\Server::get(IRootFolder::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(\OCP\Share\IManager::class),
+ ));
} else {
- $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo);
+ /** @var ObjectTree $tree */
+ $tree->init($root, $view, $this->mountManager);
}
- $objectTree->init($root, $view, $this->mountManager);
$server->addPlugin(
- new \OCA\DAV\Connector\Sabre\FilesPlugin(
- $objectTree,
+ new FilesPlugin(
+ $tree,
$this->config,
$this->request,
$this->previewManager,
$this->userSession,
+ \OCP\Server::get(IFilenameValidator::class),
+ \OCP\Server::get(IAccountManager::class),
false,
- !$this->config->getSystemValue('debug', false)
+ !$debugEnabled
)
);
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin());
+ $server->addPlugin(new QuotaPlugin($view));
+ $server->addPlugin(new ChecksumUpdatePlugin());
// Allow view-only plugin for webdav requests
$server->addPlugin(new ViewOnlyPlugin(
@@ -165,45 +199,46 @@ class ServerFactory {
));
if ($this->userSession->isLoggedIn()) {
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(
- $objectTree,
+ $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession));
+ $server->addPlugin(new SharesPlugin(
+ $tree,
$this->userSession,
$userFolder,
- \OC::$server->getShareManager()
+ \OCP\Server::get(\OCP\Share\IManager::class)
));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession));
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin(
- $objectTree,
+ $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession));
+ $server->addPlugin(new FilesReportPlugin(
+ $tree,
$view,
- \OC::$server->getSystemTagManager(),
- \OC::$server->getSystemTagObjectMapper(),
- \OC::$server->getTagManager(),
+ \OCP\Server::get(ISystemTagManager::class),
+ \OCP\Server::get(ISystemTagObjectMapper::class),
+ \OCP\Server::get(ITagManager::class),
$this->userSession,
- \OC::$server->getGroupManager(),
+ \OCP\Server::get(IGroupManager::class),
$userFolder,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
));
// custom properties plugin must be the last one
$server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
- new \OCA\DAV\DAV\CustomPropertiesBackend(
+ new CustomPropertiesBackend(
$server,
- $objectTree,
+ $tree,
$this->databaseConnection,
- $this->userSession->getUser()
+ $this->userSession->getUser(),
+ \OCP\Server::get(DefaultCalendarValidator::class),
)
)
);
}
- $server->addPlugin(new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin());
+ $server->addPlugin(new CopyEtagHeaderPlugin());
// Load dav plugins from apps
$event = new SabrePluginEvent($server);
$this->eventDispatcher->dispatchTyped($event);
$pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$server->addPlugin($appPlugin);
diff --git a/apps/dav/lib/Connector/Sabre/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php
index bacbdc99a73..0b66ed27576 100644
--- a/apps/dav/lib/Connector/Sabre/ShareTypeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -35,17 +20,14 @@ class ShareTypeList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * Share types
- *
- * @var int[]
- */
- private $shareTypes;
-
- /**
* @param int[] $shareTypes
*/
- public function __construct($shareTypes) {
- $this->shareTypes = $shareTypes;
+ public function __construct(
+ /**
+ * Share types
+ */
+ private $shareTypes,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php
index e43f552a8cc..909c29fc24b 100644
--- a/apps/dav/lib/Connector/Sabre/ShareeList.php
+++ b/apps/dav/lib/Connector/Sabre/ShareeList.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Connector\Sabre;
@@ -37,11 +18,10 @@ use Sabre\Xml\XmlSerializable;
class ShareeList implements XmlSerializable {
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
- /** @var IShare[] */
- private $shares;
-
- public function __construct(array $shares) {
- $this->shares = $shares;
+ public function __construct(
+ /** @var IShare[] */
+ private array $shares,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
index 4cda346af01..f49e85333f3 100644
--- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php
@@ -1,33 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tobias Kaminsky <tobias@kaminsky.me>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
+use OC\Share20\Exception\BackendError;
use OCA\DAV\Connector\Sabre\Node as DavNode;
use OCP\Files\Folder;
use OCP\Files\Node;
@@ -54,24 +34,19 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
* @var \Sabre\DAV\Server
*/
private $server;
- private IManager $shareManager;
- private Tree $tree;
private string $userId;
- private Folder $userFolder;
+
/** @var IShare[][] */
private array $cachedShares = [];
/** @var string[] */
private array $cachedFolders = [];
public function __construct(
- Tree $tree,
- IUserSession $userSession,
- Folder $userFolder,
- IManager $shareManager
+ private Tree $tree,
+ private IUserSession $userSession,
+ private Folder $userFolder,
+ private IManager $shareManager,
) {
- $this->tree = $tree;
- $this->shareManager = $shareManager;
- $this->userFolder = $userFolder;
$this->userId = $userSession->getUser()->getUID();
}
@@ -112,18 +87,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
IShare::TYPE_DECK,
IShare::TYPE_SCIENCEMESH,
];
+
foreach ($requestedShareTypes as $requestedShareType) {
- $shares = $this->shareManager->getSharesBy(
+ $result = array_merge($result, $this->shareManager->getSharesBy(
$this->userId,
$requestedShareType,
$node,
false,
-1
- );
- foreach ($shares as $share) {
- $result[] = $share;
+ ));
+
+ // Also check for shares where the user is the recipient
+ try {
+ $result = array_merge($result, $this->shareManager->getSharedWith(
+ $this->userId,
+ $requestedShareType,
+ $node,
+ -1
+ ));
+ } catch (BackendError $e) {
+ // ignore
}
}
+
return $result;
}
@@ -145,27 +131,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
private function getShares(DavNode $sabreNode): array {
if (isset($this->cachedShares[$sabreNode->getId()])) {
- $shares = $this->cachedShares[$sabreNode->getId()];
- } else {
- [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
- if ($parentPath === '') {
- $parentPath = '/';
- }
- // if we already cached the folder this file is in we know there are no shares for this file
- if (array_search($parentPath, $this->cachedFolders) === false) {
- try {
- $node = $sabreNode->getNode();
- } catch (NotFoundException $e) {
- return [];
- }
- $shares = $this->getShare($node);
- $this->cachedShares[$sabreNode->getId()] = $shares;
- } else {
+ return $this->cachedShares[$sabreNode->getId()];
+ }
+
+ [$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
+ if ($parentPath === '') {
+ $parentPath = '/';
+ }
+
+ // if we already cached the folder containing this file
+ // then we already know there are no shares here.
+ if (array_search($parentPath, $this->cachedFolders) === false) {
+ try {
+ $node = $sabreNode->getNode();
+ } catch (NotFoundException $e) {
return [];
}
+
+ $shares = $this->getShare($node);
+ $this->cachedShares[$sabreNode->getId()] = $shares;
+ return $shares;
}
- return $shares;
+ return [];
}
/**
@@ -176,18 +164,20 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $sabreNode
+ \Sabre\DAV\INode $sabreNode,
) {
if (!($sabreNode instanceof DavNode)) {
return;
}
- // need prefetch ?
+ // If the node is a directory and we are requesting share types or sharees
+ // then we get all the shares in the folder and cache them.
+ // This is more performant than iterating each files afterwards.
if ($sabreNode instanceof Directory
&& $propFind->getDepth() !== 0
&& (
- !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) ||
- !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME))
+ !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME))
+ || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME))
)
) {
$folderNode = $sabreNode->getNode();
diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php
index 86006cd3404..9a5cd0d51cf 100644
--- a/apps/dav/lib/Connector/Sabre/TagList.php
+++ b/apps/dav/lib/Connector/Sabre/TagList.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -36,17 +20,14 @@ class TagList implements Element {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
- * tags
- *
- * @var array
- */
- private $tags;
-
- /**
* @param array $tags
*/
- public function __construct(array $tags) {
- $this->tags = $tags;
+ public function __construct(
+ /**
+ * tags
+ */
+ private array $tags,
+ ) {
}
/**
diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
index dc2b6b0c339..25c1633df36 100644
--- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php
@@ -1,31 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
- * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sergio Bertolín <sbertolin@solidgear.es>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Connector\Sabre;
@@ -49,7 +27,10 @@ namespace OCA\DAV\Connector\Sabre;
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
-
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ITagManager;
+use OCP\ITags;
+use OCP\IUserSession;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
@@ -69,12 +50,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
private $server;
/**
- * @var \OCP\ITagManager
- */
- private $tagManager;
-
- /**
- * @var \OCP\ITags
+ * @var ITags
*/
private $tagger;
@@ -87,17 +63,15 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
private $cachedTags;
/**
- * @var \Sabre\DAV\Tree
- */
- private $tree;
-
- /**
* @param \Sabre\DAV\Tree $tree tree
- * @param \OCP\ITagManager $tagManager tag manager
+ * @param ITagManager $tagManager tag manager
*/
- public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
- $this->tree = $tree;
- $this->tagManager = $tagManager;
+ public function __construct(
+ private \Sabre\DAV\Tree $tree,
+ private ITagManager $tagManager,
+ private IEventDispatcher $eventDispatcher,
+ private IUserSession $userSession,
+ ) {
$this->tagger = null;
$this->cachedTags = [];
}
@@ -120,12 +94,13 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
$this->server = $server;
$this->server->on('propFind', [$this, 'handleGetProperties']);
$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
+ $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']);
}
/**
* Returns the tagger
*
- * @return \OCP\ITags tagger
+ * @return ITags tagger
*/
private function getTagger() {
if (!$this->tagger) {
@@ -139,7 +114,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*
* @param integer $fileId file id
* @return array list($tags, $favorite) with $tags as tag array
- * and $favorite is a boolean whether the file was favorited
+ * and $favorite is a boolean whether the file was favorited
*/
private function getTagsAndFav($fileId) {
$isFav = false;
@@ -176,6 +151,24 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
+ * Prefetches tags for a list of file IDs and caches the results
+ *
+ * @param array $fileIds List of file IDs to prefetch tags for
+ * @return void
+ */
+ private function prefetchTagsForFileIds(array $fileIds) {
+ $tags = $this->getTagger()->getTagsForObjects($fileIds);
+ if ($tags === false) {
+ // the tags API returns false on error...
+ $tags = [];
+ }
+
+ foreach ($fileIds as $fileId) {
+ $this->cachedTags[$fileId] = $tags[$fileId] ?? [];
+ }
+ }
+
+ /**
* Updates the tags of the given file id
*
* @param int $fileId
@@ -211,36 +204,25 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
// need prefetch ?
- if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
+ if ($node instanceof Directory
&& $propFind->getDepth() !== 0
&& (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
|| !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
)) {
// note: pre-fetching only supported for depth <= 1
$folderContent = $node->getChildren();
- $fileIds[] = (int)$node->getId();
+ $fileIds = [(int)$node->getId()];
foreach ($folderContent as $info) {
$fileIds[] = (int)$info->getId();
}
- $tags = $this->getTagger()->getTagsForObjects($fileIds);
- if ($tags === false) {
- // the tags API returns false on error...
- $tags = [];
- }
-
- $this->cachedTags = $this->cachedTags + $tags;
- $emptyFileIds = array_diff($fileIds, array_keys($tags));
- // also cache the ones that were not found
- foreach ($emptyFileIds as $fileId) {
- $this->cachedTags[$fileId] = [];
- }
+ $this->prefetchTagsForFileIds($fileIds);
}
$isFav = null;
@@ -272,7 +254,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->tree->getNodeForPath($path);
- if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
+ if (!($node instanceof Node)) {
return;
}
@@ -281,7 +263,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
return true;
});
- $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
+ $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) {
if ((int)$favState === 1 || $favState === 'true') {
$this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
} else {
@@ -296,4 +278,14 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
return 200;
});
}
+
+ public function handlePreloadProperties(array $nodes, array $requestProperties): void {
+ if (
+ !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true)
+ && !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true)
+ ) {
+ return;
+ }
+ $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes));
+ }
}
diff --git a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php
new file mode 100644
index 00000000000..f198519b454
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php
@@ -0,0 +1,193 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Connector\Sabre;
+
+use OC\Streamer;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\BeforeZipCreatedEvent;
+use OCP\Files\File as NcFile;
+use OCP\Files\Folder as NcFolder;
+use OCP\Files\Node as NcNode;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Tree;
+use Sabre\HTTP\Request;
+use Sabre\HTTP\Response;
+
+/**
+ * This plugin allows to download folders accessed by GET HTTP requests on DAV.
+ * The WebDAV standard explicitly say that GET is not covered and should return what ever the application thinks would be a good representation.
+ *
+ * When a collection is accessed using GET, this will provide the content as a archive.
+ * The type can be set by the `Accept` header (MIME type of zip or tar), or as browser fallback using a `accept` GET parameter.
+ * It is also possible to only include some child nodes (from the collection it self) by providing a `filter` GET parameter or `X-NC-Files` custom header.
+ */
+class ZipFolderPlugin extends ServerPlugin {
+
+ /**
+ * Reference to main server object
+ */
+ private ?Server $server = null;
+
+ public function __construct(
+ private Tree $tree,
+ private LoggerInterface $logger,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by \Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ */
+ public function initialize(Server $server): void {
+ $this->server = $server;
+ $this->server->on('method:GET', $this->handleDownload(...), 100);
+ // low priority to give any other afterMethod:* a chance to fire before we cancel everything
+ $this->server->on('afterMethod:GET', $this->afterDownload(...), 999);
+ }
+
+ /**
+ * Adding a node to the archive streamer.
+ * This will recursively add new nodes to the stream if the node is a directory.
+ */
+ protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
+ // Remove the root path from the filename to make it relative to the requested folder
+ $filename = str_replace($rootPath, '', $node->getPath());
+
+ $mtime = $node->getMTime();
+ if ($node instanceof NcFile) {
+ $resource = $node->fopen('rb');
+ if ($resource === false) {
+ $this->logger->info('Cannot read file for zip stream', ['filePath' => $node->getPath()]);
+ throw new \Sabre\DAV\Exception\ServiceUnavailable('Requested file can currently not be accessed.');
+ }
+ $streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime);
+ } elseif ($node instanceof NcFolder) {
+ $streamer->addEmptyDir($filename, $mtime);
+ $content = $node->getDirectoryListing();
+ foreach ($content as $subNode) {
+ $this->streamNode($streamer, $subNode, $rootPath);
+ }
+ }
+ }
+
+ /**
+ * Download a folder as an archive.
+ * It is possible to filter / limit the files that should be downloaded,
+ * either by passing (multiple) `X-NC-Files: the-file` headers
+ * or by setting a `files=JSON_ARRAY_OF_FILES` URL query.
+ *
+ * @return false|null
+ */
+ public function handleDownload(Request $request, Response $response): ?bool {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ if (!($node instanceof Directory)) {
+ // only handle directories
+ return null;
+ }
+
+ $query = $request->getQueryParameters();
+
+ // Get accept header - or if set overwrite with accept GET-param
+ $accept = $request->getHeaderAsArray('Accept');
+ $acceptParam = $query['accept'] ?? '';
+ if ($acceptParam !== '') {
+ $accept = array_map(fn (string $name) => strtolower(trim($name)), explode(',', $acceptParam));
+ }
+ $zipRequest = !empty(array_intersect(['application/zip', 'zip'], $accept));
+ $tarRequest = !empty(array_intersect(['application/x-tar', 'tar'], $accept));
+ if (!$zipRequest && !$tarRequest) {
+ // does not accept zip or tar stream
+ return null;
+ }
+
+ $files = $request->getHeaderAsArray('X-NC-Files');
+ $filesParam = $query['files'] ?? '';
+ // The preferred way would be headers, but this is not possible for simple browser requests ("links")
+ // so we also need to support GET parameters
+ if ($filesParam !== '') {
+ $files = json_decode($filesParam);
+ if (!is_array($files)) {
+ $files = [$files];
+ }
+
+ foreach ($files as $file) {
+ if (!is_string($file)) {
+ // we log this as this means either we - or an app - have a bug somewhere or a user is trying invalid things
+ $this->logger->notice('Invalid files filter parameter for ZipFolderPlugin', ['filter' => $filesParam]);
+ // no valid parameter so continue with Sabre behavior
+ return null;
+ }
+ }
+ }
+
+ $folder = $node->getNode();
+ $event = new BeforeZipCreatedEvent($folder, $files);
+ $this->eventDispatcher->dispatchTyped($event);
+ if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
+ $errorMessage = $event->getErrorMessage();
+ if ($errorMessage === null) {
+ // Not allowed to download but also no explaining error
+ // so we abort the ZIP creation and fall back to Sabre default behavior.
+ return null;
+ }
+ // Downloading was denied by an app
+ throw new Forbidden($errorMessage);
+ }
+
+ $content = empty($files) ? $folder->getDirectoryListing() : [];
+ foreach ($files as $path) {
+ $child = $node->getChild($path);
+ assert($child instanceof Node);
+ $content[] = $child->getNode();
+ }
+
+ $archiveName = 'download';
+ $rootPath = $folder->getPath();
+ if (empty($files)) {
+ // We download the full folder so keep it in the tree
+ $rootPath = dirname($folder->getPath());
+ // Full folder is loaded to rename the archive to the folder name
+ $archiveName = $folder->getName();
+ }
+ $streamer = new Streamer($tarRequest, -1, count($content));
+ $streamer->sendHeaders($archiveName);
+ // For full folder downloads we also add the folder itself to the archive
+ if (empty($files)) {
+ $streamer->addEmptyDir($archiveName);
+ }
+ foreach ($content as $node) {
+ $this->streamNode($streamer, $node, $rootPath);
+ }
+ $streamer->finalize();
+ return false;
+ }
+
+ /**
+ * Tell sabre/dav not to trigger it's own response sending logic as the handleDownload will have already send the response
+ *
+ * @return false|null
+ */
+ public function afterDownload(Request $request, Response $response): ?bool {
+ $node = $this->tree->getNodeForPath($request->getPath());
+ if (!($node instanceof Directory)) {
+ // only handle directories
+ return null;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/apps/dav/lib/Controller/BirthdayCalendarController.php b/apps/dav/lib/Controller/BirthdayCalendarController.php
index 5df13cefe97..f6bfb229a9c 100644
--- a/apps/dav/lib/Controller/BirthdayCalendarController.php
+++ b/apps/dav/lib/Controller/BirthdayCalendarController.php
@@ -1,31 +1,16 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Settings\CalDAVSettings;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\BackgroundJob\IJobList;
@@ -38,31 +23,6 @@ use OCP\IUserManager;
class BirthdayCalendarController extends Controller {
/**
- * @var IDBConnection
- */
- protected $db;
-
- /**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var IUserManager
- */
- protected $userManager;
-
- /**
- * @var CalDavBackend
- */
- protected $caldavBackend;
-
- /**
- * @var IJobList
- */
- protected $jobList;
-
- /**
* BirthdayCalendar constructor.
*
* @param string $appName
@@ -71,30 +31,29 @@ class BirthdayCalendarController extends Controller {
* @param IConfig $config
* @param IJobList $jobList
* @param IUserManager $userManager
- * @param CalDavBackend $calDavBackend
+ * @param CalDavBackend $caldavBackend
*/
- public function __construct($appName, IRequest $request,
- IDBConnection $db, IConfig $config,
- IJobList $jobList,
- IUserManager $userManager,
- CalDavBackend $calDavBackend) {
+ public function __construct(
+ $appName,
+ IRequest $request,
+ protected IDBConnection $db,
+ protected IConfig $config,
+ protected IJobList $jobList,
+ protected IUserManager $userManager,
+ protected CalDavBackend $caldavBackend,
+ ) {
parent::__construct($appName, $request);
- $this->db = $db;
- $this->config = $config;
- $this->userManager = $userManager;
- $this->jobList = $jobList;
- $this->caldavBackend = $calDavBackend;
}
/**
* @return Response
- * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings)
*/
+ #[AuthorizedAdminSetting(settings: CalDAVSettings::class)]
public function enable() {
$this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'yes');
// add background job for each user
- $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->userManager->callForSeenUsers(function (IUser $user): void {
$this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [
'userId' => $user->getUID(),
]);
@@ -105,8 +64,8 @@ class BirthdayCalendarController extends Controller {
/**
* @return Response
- * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings)
*/
+ #[AuthorizedAdminSetting(settings: CalDAVSettings::class)]
public function disable() {
$this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'no');
diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php
index 2d4522c8d37..ea209168123 100644
--- a/apps/dav/lib/Controller/DirectController.php
+++ b/apps/dav/lib/Controller/DirectController.php
@@ -3,33 +3,15 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Iscle <albertiscle9@gmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\Db\Direct;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
@@ -46,50 +28,21 @@ use OCP\Security\ISecureRandom;
class DirectController extends OCSController {
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var string */
- private $userId;
-
- /** @var DirectMapper */
- private $mapper;
-
- /** @var ISecureRandom */
- private $random;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
- public function __construct(string $appName,
+ public function __construct(
+ string $appName,
IRequest $request,
- IRootFolder $rootFolder,
- string $userId,
- DirectMapper $mapper,
- ISecureRandom $random,
- ITimeFactory $timeFactory,
- IURLGenerator $urlGenerator,
- IEventDispatcher $eventDispatcher) {
+ private IRootFolder $rootFolder,
+ private string $userId,
+ private DirectMapper $mapper,
+ private ISecureRandom $random,
+ private ITimeFactory $timeFactory,
+ private IURLGenerator $urlGenerator,
+ private IEventDispatcher $eventDispatcher,
+ ) {
parent::__construct($appName, $request);
-
- $this->rootFolder = $rootFolder;
- $this->userId = $userId;
- $this->mapper = $mapper;
- $this->random = $random;
- $this->timeFactory = $timeFactory;
- $this->urlGenerator = $urlGenerator;
- $this->eventDispatcher = $eventDispatcher;
}
/**
- * @NoAdminRequired
- *
* Get a direct link to a file
*
* @param int $fileId ID of the file
@@ -101,6 +54,7 @@ class DirectController extends OCSController {
*
* 200: Direct link returned
*/
+ #[NoAdminRequired]
public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
@@ -136,7 +90,7 @@ class DirectController extends OCSController {
$this->mapper->insert($direct);
- $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/'.$token);
+ $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/' . $token);
return new DataResponse([
'url' => $url,
diff --git a/apps/dav/lib/Controller/ExampleContentController.php b/apps/dav/lib/Controller/ExampleContentController.php
new file mode 100644
index 00000000000..e20ee4b7f49
--- /dev/null
+++ b/apps/dav/lib/Controller/ExampleContentController.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\AppFramework\ApiController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\FrontpageRoute;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\DataDownloadResponse;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+class ExampleContentController extends ApiController {
+ public function __construct(
+ IRequest $request,
+ private readonly LoggerInterface $logger,
+ private readonly ExampleEventService $exampleEventService,
+ private readonly ExampleContactService $exampleContactService,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/config')]
+ public function setEnableDefaultContact(bool $allow): JSONResponse {
+ if ($allow && !$this->exampleContactService->defaultContactExists()) {
+ try {
+ $this->exampleContactService->setCard();
+ } catch (\Exception $e) {
+ $this->logger->error('Could not create default contact', ['exception' => $e]);
+ return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ }
+ $this->exampleContactService->setDefaultContactEnabled($allow);
+ return new JSONResponse([], Http::STATUS_OK);
+ }
+
+ #[NoCSRFRequired]
+ #[FrontpageRoute(verb: 'GET', url: '/api/defaultcontact/contact')]
+ public function getDefaultContact(): DataDownloadResponse {
+ $cardData = $this->exampleContactService->getCard()
+ ?? file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf');
+ return new DataDownloadResponse($cardData, 'example_contact.vcf', 'text/vcard');
+ }
+
+ #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/contact')]
+ public function setDefaultContact(?string $contactData = null) {
+ if (!$this->exampleContactService->isDefaultContactEnabled()) {
+ return new JSONResponse([], Http::STATUS_FORBIDDEN);
+ }
+ $this->exampleContactService->setCard($contactData);
+ return new JSONResponse([], Http::STATUS_OK);
+ }
+
+ #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')]
+ public function setCreateExampleEvent(bool $enable): JSONResponse {
+ $this->exampleEventService->setCreateExampleEvent($enable);
+ return new JsonResponse([]);
+ }
+
+ #[FrontpageRoute(verb: 'GET', url: '/api/exampleEvent/event')]
+ #[NoCSRFRequired]
+ public function downloadExampleEvent(): DataDownloadResponse {
+ $exampleEvent = $this->exampleEventService->getExampleEvent();
+ return new DataDownloadResponse(
+ $exampleEvent->getIcs(),
+ 'example_event.ics',
+ 'text/calendar',
+ );
+ }
+
+ #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/event')]
+ public function uploadExampleEvent(string $ics): JSONResponse {
+ if (!$this->exampleEventService->shouldCreateExampleEvent()) {
+ return new JSONResponse([], Http::STATUS_FORBIDDEN);
+ }
+
+ $this->exampleEventService->saveCustomExampleEvent($ics);
+ return new JsonResponse([]);
+ }
+
+ #[FrontpageRoute(verb: 'DELETE', url: '/api/exampleEvent/event')]
+ public function deleteExampleEvent(): JSONResponse {
+ $this->exampleEventService->deleteCustomExampleEvent();
+ return new JsonResponse([]);
+ }
+
+}
diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php
index 4a32966220f..19eb4097b45 100644
--- a/apps/dav/lib/Controller/InvitationResponseController.php
+++ b/apps/dav/lib/Controller/InvitationResponseController.php
@@ -3,34 +3,16 @@
declare(strict_types=1);
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
@@ -41,15 +23,6 @@ use Sabre\VObject\Reader;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class InvitationResponseController extends Controller {
- /** @var IDBConnection */
- private $db;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var InvitationResponseServer */
- private $responseServer;
-
/**
* InvitationResponseController constructor.
*
@@ -59,25 +32,25 @@ class InvitationResponseController extends Controller {
* @param ITimeFactory $timeFactory
* @param InvitationResponseServer $responseServer
*/
- public function __construct(string $appName, IRequest $request,
- IDBConnection $db, ITimeFactory $timeFactory,
- InvitationResponseServer $responseServer) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IDBConnection $db,
+ private ITimeFactory $timeFactory,
+ private InvitationResponseServer $responseServer,
+ ) {
parent::__construct($appName, $request);
- $this->db = $db;
- $this->timeFactory = $timeFactory;
- $this->responseServer = $responseServer;
// Don't run `$server->exec()`, because we just need access to the
// fully initialized schedule plugin, but we don't want Sabre/DAV
// to actually handle and reply to the request
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function accept(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
@@ -96,12 +69,11 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function decline(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
@@ -121,12 +93,11 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function options(string $token):TemplateResponse {
return new TemplateResponse($this->appName, 'schedule-response-options', [
'token' => $token
@@ -134,13 +105,12 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
*
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function processMoreOptionsResult(string $token):TemplateResponse {
$partstat = $this->request->getParam('partStat');
@@ -178,7 +148,7 @@ class InvitationResponseController extends Controller {
}
$currentTime = $this->timeFactory->getTime();
- if (((int) $row['expiration']) < $currentTime) {
+ if (((int)$row['expiration']) < $currentTime) {
return null;
}
diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php
index 5ab99c34b79..d3516d092e8 100644
--- a/apps/dav/lib/Controller/OutOfOfficeController.php
+++ b/apps/dav/lib/Controller/OutOfOfficeController.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
@@ -38,6 +21,7 @@ use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\User\IAvailabilityCoordinator;
+use function mb_strlen;
/**
* @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions
@@ -110,6 +94,8 @@ class OutOfOfficeController extends OCSController {
'lastDay' => $data->getLastDay(),
'status' => $data->getStatus(),
'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
]);
}
@@ -120,11 +106,13 @@ class OutOfOfficeController extends OCSController {
* @param string $lastDay Last day of the absence in format `YYYY-MM-DD`
* @param string $status Short text that is set as user status during the absence
* @param string $message Longer multiline message that is shown to others during the absence
- * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>
+ * @param ?string $replacementUserId User id of the replacement user
+ * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'|'statusLength'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Absence data
- * 400: When the first day is not before the last day
+ * 400: When validation fails, e.g. data range error or the first day is not before the last day
* 401: When the user is not logged in
+ * 404: When the replacementUserId was provided but replacement user was not found
*/
#[NoAdminRequired]
public function setOutOfOffice(
@@ -132,11 +120,23 @@ class OutOfOfficeController extends OCSController {
string $lastDay,
string $status,
string $message,
+ ?string $replacementUserId,
): DataResponse {
$user = $this->userSession?->getUser();
if ($user === null) {
return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
}
+ if (mb_strlen($status) > 100) {
+ return new DataResponse(['error' => 'statusLength'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $replacementUser = null;
+ if ($replacementUserId !== null) {
+ $replacementUser = $this->userManager->get($replacementUserId);
+ if ($replacementUser === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ }
$parsedFirstDay = new DateTimeImmutable($firstDay);
$parsedLastDay = new DateTimeImmutable($lastDay);
@@ -150,6 +150,8 @@ class OutOfOfficeController extends OCSController {
$lastDay,
$status,
$message,
+ $replacementUserId,
+ $replacementUser?->getDisplayName()
);
$this->coordinator->clearCache($user->getUID());
@@ -160,6 +162,8 @@ class OutOfOfficeController extends OCSController {
'lastDay' => $data->getLastDay(),
'status' => $data->getStatus(),
'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
]);
}
diff --git a/apps/dav/lib/Controller/UpcomingEventsController.php b/apps/dav/lib/Controller/UpcomingEventsController.php
new file mode 100644
index 00000000000..a5d54f44754
--- /dev/null
+++ b/apps/dav/lib/Controller/UpcomingEventsController.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\UpcomingEvent;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCA\DAV\ResponseDefinitions;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+
+/**
+ * @psalm-import-type DAVUpcomingEvent from ResponseDefinitions
+ */
+class UpcomingEventsController extends OCSController {
+ public function __construct(
+ IRequest $request,
+ private ?string $userId,
+ private UpcomingEventsService $service,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ /**
+ * Get information about upcoming events
+ *
+ * @param string|null $location location/URL to filter by
+ * @return DataResponse<Http::STATUS_OK, array{events: list<DAVUpcomingEvent>}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>
+ *
+ * 200: Upcoming events
+ * 401: When not authenticated
+ */
+ #[NoAdminRequired]
+ public function getEvents(?string $location = null): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
+ }
+
+ return new DataResponse([
+ 'events' => array_values(array_map(fn (UpcomingEvent $e) => $e->jsonSerialize(), $this->service->getEvents(
+ $this->userId,
+ $location,
+ ))),
+ ]);
+ }
+
+}
diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php
index 48872048ea8..f9a4f8ee986 100644
--- a/apps/dav/lib/DAV/CustomPropertiesBackend.php
+++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php
@@ -1,38 +1,21 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
use Exception;
+use OCA\DAV\CalDAV\Calendar;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
use OCA\DAV\Connector\Sabre\Directory;
-use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
-use Sabre\CalDAV\ICalendar;
use Sabre\DAV\Exception as DavException;
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
use Sabre\DAV\PropFind;
@@ -83,33 +66,16 @@ class CustomPropertiesBackend implements BackendInterface {
'{DAV:}getetag',
'{DAV:}quota-used-bytes',
'{DAV:}quota-available-bytes',
- '{http://owncloud.org/ns}permissions',
- '{http://owncloud.org/ns}downloadURL',
- '{http://owncloud.org/ns}dDC',
- '{http://owncloud.org/ns}size',
- '{http://nextcloud.org/ns}is-encrypted',
-
- // Currently, returning null from any propfind handler would still trigger the backend,
- // so we add all known Nextcloud custom properties in here to avoid that
-
- // text app
- '{http://nextcloud.org/ns}rich-workspace',
- '{http://nextcloud.org/ns}rich-workspace-file',
- // groupfolders
- '{http://nextcloud.org/ns}acl-enabled',
- '{http://nextcloud.org/ns}acl-can-manage',
- '{http://nextcloud.org/ns}acl-list',
- '{http://nextcloud.org/ns}inherited-acl-list',
- '{http://nextcloud.org/ns}group-folder-id',
- // files_lock
- '{http://nextcloud.org/ns}lock',
- '{http://nextcloud.org/ns}lock-owner-type',
- '{http://nextcloud.org/ns}lock-owner',
- '{http://nextcloud.org/ns}lock-owner-displayname',
- '{http://nextcloud.org/ns}lock-owner-editor',
- '{http://nextcloud.org/ns}lock-time',
- '{http://nextcloud.org/ns}lock-timeout',
- '{http://nextcloud.org/ns}lock-token',
+ ];
+
+ /**
+ * Allowed properties for the oc/nc namespace, all other properties in the namespace are ignored
+ *
+ * @var string[]
+ */
+ private const ALLOWED_NC_PROPERTIES = [
+ '{http://owncloud.org/ns}calendar-enabled',
+ '{http://owncloud.org/ns}enabled',
];
/**
@@ -131,28 +97,11 @@ class CustomPropertiesBackend implements BackendInterface {
];
/**
- * @var Tree
- */
- private $tree;
-
- /**
- * @var IDBConnection
- */
- private $connection;
-
- /**
- * @var IUser
- */
- private $user;
-
- /**
* Properties cache
*
* @var array
*/
private $userCache = [];
-
- private Server $server;
private XmlService $xmlService;
/**
@@ -161,15 +110,12 @@ class CustomPropertiesBackend implements BackendInterface {
* @param IUser $user owner of the tree and properties
*/
public function __construct(
- Server $server,
- Tree $tree,
- IDBConnection $connection,
- IUser $user,
+ private Server $server,
+ private Tree $tree,
+ private IDBConnection $connection,
+ private IUser $user,
+ private DefaultCalendarValidator $defaultCalendarValidator,
) {
- $this->server = $server;
- $this->tree = $tree;
- $this->connection = $connection;
- $this->user = $user;
$this->xmlService = new XmlService();
$this->xmlService->elementMap = array_merge(
$this->xmlService->elementMap,
@@ -187,14 +133,9 @@ class CustomPropertiesBackend implements BackendInterface {
public function propFind($path, PropFind $propFind) {
$requestedProps = $propFind->get404Properties();
- // these might appear
- $requestedProps = array_diff(
- $requestedProps,
- self::IGNORED_PROPERTIES,
- );
$requestedProps = array_filter(
$requestedProps,
- fn ($prop) => !str_starts_with($prop, FilesPlugin::FILE_METADATA_PREFIX),
+ $this->isPropertyAllowed(...),
);
// substr of calendars/ => path is inside the CalDAV component
@@ -256,6 +197,11 @@ class CustomPropertiesBackend implements BackendInterface {
$this->cacheDirectory($path, $node);
}
+ if ($node instanceof CalendarObject) {
+ // No custom properties supported on individual events
+ return;
+ }
+
// First fetch the published properties (set by another user), then get the ones set by
// the current user. If both are set then the latter as priority.
foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
@@ -276,6 +222,16 @@ class CustomPropertiesBackend implements BackendInterface {
}
}
+ private function isPropertyAllowed(string $property): bool {
+ if (in_array($property, self::IGNORED_PROPERTIES)) {
+ return false;
+ }
+ if (str_starts_with($property, '{http://owncloud.org/ns}') || str_starts_with($property, '{http://nextcloud.org/ns}')) {
+ return in_array($property, self::ALLOWED_NC_PROPERTIES);
+ }
+ return true;
+ }
+
/**
* Updates properties for a path
*
@@ -315,8 +271,8 @@ class CustomPropertiesBackend implements BackendInterface {
*/
public function move($source, $destination) {
$statement = $this->connection->prepare(
- 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
- ' WHERE `userid` = ? AND `propertypath` = ?'
+ 'UPDATE `*PREFIX*properties` SET `propertypath` = ?'
+ . ' WHERE `userid` = ? AND `propertypath` = ?'
);
$statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
$statement->closeCursor();
@@ -338,10 +294,11 @@ class CustomPropertiesBackend implements BackendInterface {
// $path is the principal here as this prop is only set on principals
$node = $this->tree->getNodeForPath($href);
- if (!($node instanceof ICalendar) || $node->getOwner() !== $path) {
+ if (!($node instanceof Calendar) || $node->getOwner() !== $path) {
throw new DavException('No such calendar');
}
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
break;
}
}
@@ -378,16 +335,19 @@ class CustomPropertiesBackend implements BackendInterface {
private function cacheDirectory(string $path, Directory $node): void {
$prefix = ltrim($path . '/', '/');
$query = $this->connection->getQueryBuilder();
- $query->select('name', 'propertypath', 'propertyname', 'propertyvalue', 'valuetype')
+ $query->select('name', 'p.propertypath', 'p.propertyname', 'p.propertyvalue', 'p.valuetype')
->from('filecache', 'f')
- ->leftJoin('f', 'properties', 'p', $query->expr()->andX(
- $query->expr()->eq('propertypath', $query->func()->concat(
- $query->createNamedParameter($prefix),
- 'name'
- )),
- $query->expr()->eq('userid', $query->createNamedParameter($this->user->getUID()))
- ))
- ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT)));
+ ->hintShardKey('storage', $node->getNode()->getMountPoint()->getNumericStorageId())
+ ->leftJoin('f', 'properties', 'p', $query->expr()->eq('p.propertypath', $query->func()->concat(
+ $query->createNamedParameter($prefix),
+ 'f.name'
+ )),
+ )
+ ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT)))
+ ->andWhere($query->expr()->orX(
+ $query->expr()->eq('p.userid', $query->createNamedParameter($this->user->getUID())),
+ $query->expr()->isNull('p.userid'),
+ ));
$result = $query->executeQuery();
$propsByPath = [];
@@ -430,7 +390,7 @@ class CustomPropertiesBackend implements BackendInterface {
// request only a subset
$sql .= ' AND `propertyname` in (?)';
$whereValues[] = $requestedProperties;
- $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
+ $whereTypes[] = IQueryBuilder::PARAM_STR_ARRAY;
}
$result = $this->connection->executeQuery(
@@ -556,7 +516,9 @@ class CustomPropertiesBackend implements BackendInterface {
$value = $value->getHref();
} else {
$valueType = self::PROPERTY_TYPE_OBJECT;
- $value = serialize($value);
+ // serialize produces null character
+ // these can not be properly stored in some databases and need to be replaced
+ $value = str_replace(chr(0), '\x00', serialize($value));
}
return [$value, $valueType];
}
@@ -571,7 +533,9 @@ class CustomPropertiesBackend implements BackendInterface {
case self::PROPERTY_TYPE_HREF:
return new Href($value);
case self::PROPERTY_TYPE_OBJECT:
- return unserialize($value);
+ // some databases can not handel null characters, these are custom encoded during serialization
+ // this custom encoding needs to be first reversed before unserializing
+ return unserialize(str_replace('\x00', chr(0), $value));
case self::PROPERTY_TYPE_STRING:
default:
return $value;
diff --git a/apps/dav/lib/DAV/GroupPrincipalBackend.php b/apps/dav/lib/DAV/GroupPrincipalBackend.php
index 8c126e6b71c..77ba45182c9 100644
--- a/apps/dav/lib/DAV/GroupPrincipalBackend.php
+++ b/apps/dav/lib/DAV/GroupPrincipalBackend.php
@@ -1,30 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
@@ -42,32 +21,17 @@ use Sabre\DAVACL\PrincipalBackend\BackendInterface;
class GroupPrincipalBackend implements BackendInterface {
public const PRINCIPAL_PREFIX = 'principals/groups';
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IShareManager */
- private $shareManager;
- /** @var IConfig */
- private $config;
-
/**
- * @param IGroupManager $IGroupManager
+ * @param IGroupManager $groupManager
* @param IUserSession $userSession
* @param IShareManager $shareManager
*/
public function __construct(
- IGroupManager $IGroupManager,
- IUserSession $userSession,
- IShareManager $shareManager,
- IConfig $config
+ private IGroupManager $groupManager,
+ private IUserSession $userSession,
+ private IShareManager $shareManager,
+ private IConfig $config,
) {
- $this->groupManager = $IGroupManager;
- $this->userSession = $userSession;
- $this->shareManager = $shareManager;
- $this->config = $config;
}
/**
@@ -87,8 +51,10 @@ class GroupPrincipalBackend implements BackendInterface {
$principals = [];
if ($prefixPath === self::PRINCIPAL_PREFIX) {
- foreach ($this->groupManager->search('') as $user) {
- $principals[] = $this->groupToPrincipal($user);
+ foreach ($this->groupManager->search('') as $group) {
+ if (!$group->hideFromCollaboration()) {
+ $principals[] = $this->groupToPrincipal($group);
+ }
}
}
@@ -114,7 +80,7 @@ class GroupPrincipalBackend implements BackendInterface {
$name = urldecode($elements[2]);
$group = $this->groupManager->get($name);
- if (!is_null($group)) {
+ if ($group !== null && !$group->hideFromCollaboration()) {
return $this->groupToPrincipal($group);
}
@@ -223,6 +189,10 @@ class GroupPrincipalBackend implements BackendInterface {
$groups = $this->groupManager->search($value, $searchLimit);
$results[] = array_reduce($groups, function (array $carry, IGroup $group) use ($restrictGroups) {
+ if ($group->hideFromCollaboration()) {
+ return $carry;
+ }
+
$gid = $group->getGID();
// is sharing restricted to groups only?
if ($restrictGroups !== false) {
diff --git a/apps/dav/lib/DAV/PublicAuth.php b/apps/dav/lib/DAV/PublicAuth.php
index 3ba8bb2f3c5..c2b4ada173a 100644
--- a/apps/dav/lib/DAV/PublicAuth.php
+++ b/apps/dav/lib/DAV/PublicAuth.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
@@ -68,9 +53,9 @@ class PublicAuth implements BackendInterface {
*/
public function check(RequestInterface $request, ResponseInterface $response) {
if ($this->isRequestPublic($request)) {
- return [true, "principals/system/public"];
+ return [true, 'principals/system/public'];
}
- return [false, "No public access to this resource."];
+ return [false, 'No public access to this resource.'];
}
/**
diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php
index b467479bc1e..d60f5cca7c6 100644
--- a/apps/dav/lib/DAV/Sharing/Backend.php
+++ b/apps/dav/lib/DAV/Sharing/Backend.php
@@ -2,32 +2,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing;
@@ -50,7 +27,8 @@ abstract class Backend {
private ICache $shareCache;
- public function __construct(private IUserManager $userManager,
+ public function __construct(
+ private IUserManager $userManager,
private IGroupManager $groupManager,
private Principal $principalBackend,
private ICacheFactory $cacheFactory,
@@ -81,13 +59,13 @@ abstract class Backend {
}
// Don't add share for owner
- if($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
continue;
}
$principalparts[2] = urldecode($principalparts[2]);
- if (($principalparts[1] === 'users' && !$this->userManager->userExists($principalparts[2])) ||
- ($principalparts[1] === 'groups' && !$this->groupManager->groupExists($principalparts[2]))) {
+ if (($principalparts[1] === 'users' && !$this->userManager->userExists($principalparts[2]))
+ || ($principalparts[1] === 'groups' && !$this->groupManager->groupExists($principalparts[2]))) {
// User or group does not exist
continue;
}
@@ -106,20 +84,12 @@ abstract class Backend {
}
// Don't add unshare for owner
- if($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
+ if ($shareable->getOwner() !== null && strcasecmp($shareable->getOwner(), $principal) === 0) {
continue;
}
// Delete any possible direct shares (since the frontend does not separate between them)
$this->service->deleteShare($shareable->getResourceId(), $principal);
-
- // Check if a user has a groupshare that they're trying to free themselves from
- // If so we need to add a self::ACCESS_UNSHARED row
- if(!str_contains($principal, 'group')
- && $this->service->hasGroupShare($oldShares)
- ) {
- $this->service->unshare($shareable->getResourceId(), $principal);
- }
}
}
@@ -153,15 +123,15 @@ abstract class Backend {
$rows = $this->service->getShares($resourceId);
$shares = [];
- foreach($rows as $row) {
+ foreach ($rows as $row) {
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
$shares[] = [
'href' => "principal:{$row['principaluri']}",
'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
'status' => 1,
- 'readOnly' => (int) $row['access'] === Backend::ACCESS_READ,
+ 'readOnly' => (int)$row['access'] === Backend::ACCESS_READ,
'{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
- '{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups')
+ '{http://owncloud.org/ns}group-share' => isset($p['uri']) && (str_starts_with($p['uri'], 'principals/groups') || str_starts_with($p['uri'], 'principals/circles'))
];
}
$this->shareCache->set((string)$resourceId, $shares);
@@ -178,14 +148,14 @@ abstract class Backend {
$rows = $this->service->getSharesForIds($resourceIds);
$sharesByResource = array_fill_keys($resourceIds, []);
- foreach($rows as $row) {
+ foreach ($rows as $row) {
$resourceId = (int)$row['resourceid'];
$p = $this->principalBackend->getPrincipalByPath($row['principaluri']);
$sharesByResource[$resourceId][] = [
'href' => "principal:{$row['principaluri']}",
'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '',
'status' => 1,
- 'readOnly' => (int) $row['access'] === self::ACCESS_READ,
+ 'readOnly' => (int)$row['access'] === self::ACCESS_READ,
'{http://owncloud.org/ns}principal' => (string)$row['principaluri'],
'{http://owncloud.org/ns}group-share' => isset($p['uri']) && str_starts_with($p['uri'], 'principals/groups')
];
@@ -226,4 +196,45 @@ abstract class Backend {
}
return $acl;
}
+
+ public function unshare(IShareable $shareable, string $principalUri): bool {
+ $this->shareCache->clear();
+
+ $principal = $this->principalBackend->findByUri($principalUri, '');
+ if (empty($principal)) {
+ return false;
+ }
+
+ if ($shareable->getOwner() === $principal) {
+ return false;
+ }
+
+ // Delete any possible direct shares (since the frontend does not separate between them)
+ $this->service->deleteShare($shareable->getResourceId(), $principal);
+
+ $needsUnshare = $this->hasAccessByGroupOrCirclesMembership(
+ $shareable->getResourceId(),
+ $principal
+ );
+
+ if ($needsUnshare) {
+ $this->service->unshare($shareable->getResourceId(), $principal);
+ }
+
+ return true;
+ }
+
+ private function hasAccessByGroupOrCirclesMembership(int $resourceId, string $principal) {
+ $memberships = array_merge(
+ $this->principalBackend->getGroupMembership($principal, true),
+ $this->principalBackend->getCircleMembership($principal)
+ );
+
+ $shares = array_column(
+ $this->service->getShares($resourceId),
+ 'principaluri'
+ );
+
+ return count(array_intersect($memberships, $shares)) > 0;
+ }
}
diff --git a/apps/dav/lib/DAV/Sharing/IShareable.php b/apps/dav/lib/DAV/Sharing/IShareable.php
index 759981af078..d83079f6975 100644
--- a/apps/dav/lib/DAV/Sharing/IShareable.php
+++ b/apps/dav/lib/DAV/Sharing/IShareable.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing;
diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php
index 78e086bc907..03e63813bab 100644
--- a/apps/dav/lib/DAV/Sharing/Plugin.php
+++ b/apps/dav/lib/DAV/Sharing/Plugin.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing;
@@ -29,6 +12,7 @@ use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\DAV\Sharing\Xml\Invite;
use OCA\DAV\DAV\Sharing\Xml\ShareRequest;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
@@ -43,26 +27,18 @@ class Plugin extends ServerPlugin {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.com/ns';
- /** @var Auth */
- private $auth;
-
- /** @var IRequest */
- private $request;
-
- /** @var IConfig */
- private $config;
-
/**
* Plugin constructor.
*
- * @param Auth $authBackEnd
+ * @param Auth $auth
* @param IRequest $request
* @param IConfig $config
*/
- public function __construct(Auth $authBackEnd, IRequest $request, IConfig $config) {
- $this->auth = $authBackEnd;
- $this->request = $request;
- $this->config = $config;
+ public function __construct(
+ private Auth $auth,
+ private IRequest $request,
+ private IConfig $config,
+ ) {
}
/**
@@ -127,7 +103,7 @@ class Plugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = (string) $request->getHeader('Content-Type');
+ $contentType = (string)$request->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -182,7 +158,7 @@ class Plugin extends ServerPlugin {
$node->updateShares($message->set, $message->remove);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
diff --git a/apps/dav/lib/DAV/Sharing/SharingMapper.php b/apps/dav/lib/DAV/Sharing/SharingMapper.php
index c0c939c7a5e..e4722208189 100644
--- a/apps/dav/lib/DAV/Sharing/SharingMapper.php
+++ b/apps/dav/lib/DAV/Sharing/SharingMapper.php
@@ -1,49 +1,49 @@
<?php
declare(strict_types=1);
-/*
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\DAV\DAV\Sharing;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class SharingMapper {
- public function __construct(private IDBConnection $db) {
+ public function __construct(
+ private IDBConnection $db,
+ ) {
}
- public function getSharesForId(int $resourceId, string $resourceType): array {
+ protected function getSharesForIdByAccess(int $resourceId, string $resourceType, bool $sharesWithAccess): array {
$query = $this->db->getQueryBuilder();
- $result = $query->select(['principaluri', 'access'])
+ $query->select(['principaluri', 'access'])
->from('dav_shares')
->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType, IQueryBuilder::PARAM_STR)))
- ->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
- ->groupBy(['principaluri', 'access'])
- ->executeQuery();
+ ->groupBy(['principaluri', 'access']);
+ if ($sharesWithAccess) {
+ $query->andWhere($query->expr()->neq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)));
+ } else {
+ $query->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)));
+ }
+
+ $result = $query->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
return $rows;
}
+ public function getSharesForId(int $resourceId, string $resourceType): array {
+ return $this->getSharesForIdByAccess($resourceId, $resourceType, true);
+ }
+
+ public function getUnsharesForId(int $resourceId, string $resourceType): array {
+ return $this->getSharesForIdByAccess($resourceId, $resourceType, false);
+ }
+
public function getSharesForIds(array $resourceIds, string $resourceType): array {
$query = $this->db->getQueryBuilder();
$result = $query->select(['resourceid', 'principaluri', 'access'])
@@ -110,4 +110,28 @@ class SharingMapper {
->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
->executeStatement();
}
+
+ public function getSharesByPrincipals(array $principals, string $resourceType): array {
+ $query = $this->db->getQueryBuilder();
+ $result = $query->select(['id', 'principaluri', 'type', 'access', 'resourceid'])
+ ->from('dav_shares')
+ ->where($query->expr()->in('principaluri', $query->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->orderBy('id')
+ ->executeQuery();
+
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return $rows;
+ }
+
+ public function deleteUnsharesByPrincipal(string $principal, string $resourceType): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
+ ->andWhere($query->expr()->eq('type', $query->createNamedParameter($resourceType)))
+ ->andWhere($query->expr()->eq('access', $query->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
+ }
}
diff --git a/apps/dav/lib/DAV/Sharing/SharingService.php b/apps/dav/lib/DAV/Sharing/SharingService.php
index 4b2a0beed1c..11459e12d74 100644
--- a/apps/dav/lib/DAV/Sharing/SharingService.php
+++ b/apps/dav/lib/DAV/Sharing/SharingService.php
@@ -2,28 +2,16 @@
declare(strict_types=1);
/**
- * @copyright 2024 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\DAV\Sharing;
abstract class SharingService {
protected string $resourceType = '';
- public function __construct(protected SharingMapper $mapper) {
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
}
public function getResourceType(): string {
@@ -55,17 +43,11 @@ abstract class SharingService {
return $this->mapper->getSharesForId($resourceId, $this->getResourceType());
}
- public function getSharesForIds(array $resourceIds): array {
- return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType());
+ public function getUnshares(int $resourceId): array {
+ return $this->mapper->getUnsharesForId($resourceId, $this->getResourceType());
}
- /**
- * @param array $oldShares
- * @return bool
- */
- public function hasGroupShare(array $oldShares): bool {
- return !empty(array_filter($oldShares, function (array $share) {
- return $share['{http://owncloud.org/ns}group-share'] === true;
- }));
+ public function getSharesForIds(array $resourceIds): array {
+ return $this->mapper->getSharesForIds($resourceIds, $this->getResourceType());
}
}
diff --git a/apps/dav/lib/DAV/Sharing/Xml/Invite.php b/apps/dav/lib/DAV/Sharing/Xml/Invite.php
index 3c03dfa5d25..7a20dbe6df7 100644
--- a/apps/dav/lib/DAV/Sharing/Xml/Invite.php
+++ b/apps/dav/lib/DAV/Sharing/Xml/Invite.php
@@ -1,28 +1,10 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
- * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-FileCopyrightText: fruux GmbH (https://fruux.com/)
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing\Xml;
@@ -45,21 +27,6 @@ use Sabre\Xml\XmlSerializable;
class Invite implements XmlSerializable {
/**
- * The list of users a calendar has been shared to.
- *
- * @var array
- */
- protected $users;
-
- /**
- * The organizer contains information about the person who shared the
- * object.
- *
- * @var array|null
- */
- protected $organizer;
-
- /**
* Creates the property.
*
* Users is an array. Each element of the array has the following
@@ -85,9 +52,17 @@ class Invite implements XmlSerializable {
*
* @param array $users
*/
- public function __construct(array $users, ?array $organizer = null) {
- $this->users = $users;
- $this->organizer = $organizer;
+ public function __construct(
+ /**
+ * The list of users a calendar has been shared to.
+ */
+ protected array $users,
+ /**
+ * The organizer contains information about the person who shared the
+ * object.
+ */
+ protected ?array $organizer = null,
+ ) {
}
/**
diff --git a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
index 6a97cd30d9f..aefb39c5701 100644
--- a/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
+++ b/apps/dav/lib/DAV/Sharing/Xml/ShareRequest.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV\Sharing\Xml;
@@ -27,24 +12,21 @@ use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class ShareRequest implements XmlDeserializable {
- public $set = [];
-
- public $remove = [];
-
/**
* Constructor
*
* @param array $set
* @param array $remove
*/
- public function __construct(array $set, array $remove) {
- $this->set = $set;
- $this->remove = $remove;
+ public function __construct(
+ public array $set,
+ public array $remove,
+ ) {
}
public static function xmlDeserialize(Reader $reader) {
$elements = $reader->parseInnerTree([
- '{' . Plugin::NS_OWNCLOUD. '}set' => 'Sabre\\Xml\\Element\\KeyValue',
+ '{' . Plugin::NS_OWNCLOUD . '}set' => 'Sabre\\Xml\\Element\\KeyValue',
'{' . Plugin::NS_OWNCLOUD . '}remove' => 'Sabre\\Xml\\Element\\KeyValue',
]);
diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php
index d5739212e86..9760d68f05f 100644
--- a/apps/dav/lib/DAV/SystemPrincipalBackend.php
+++ b/apps/dav/lib/DAV/SystemPrincipalBackend.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php
index 389dd96efb4..9b9615b8063 100644
--- a/apps/dav/lib/DAV/ViewOnlyPlugin.php
+++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php
@@ -1,22 +1,9 @@
<?php
+
/**
- * @author Piotr Mrowczynski piotr@owncloud.com
- *
- * @copyright Copyright (c) 2019, ownCloud GmbH
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2019 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\DAV;
@@ -26,6 +13,7 @@ use OCA\DAV\Connector\Sabre\File as DavFile;
use OCA\Files_Versions\Sabre\VersionFile;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
+use OCP\Files\Storage\ISharedStorage;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
@@ -36,12 +24,10 @@ use Sabre\HTTP\RequestInterface;
*/
class ViewOnlyPlugin extends ServerPlugin {
private ?Server $server = null;
- private ?Folder $userFolder;
public function __construct(
- ?Folder $userFolder,
+ private ?Folder $userFolder,
) {
- $this->userFolder = $userFolder;
}
/**
@@ -58,6 +44,7 @@ class ViewOnlyPlugin extends ServerPlugin {
//Sabre\DAV\CorePlugin::httpGet
$this->server->on('method:GET', [$this, 'checkViewOnly'], 90);
$this->server->on('method:COPY', [$this, 'checkViewOnly'], 90);
+ $this->server->on('method:MOVE', [$this, 'checkViewOnly'], 90);
}
/**
@@ -85,7 +72,7 @@ class ViewOnlyPlugin extends ServerPlugin {
$nodes = $this->userFolder->getById($node->getId());
$node = array_pop($nodes);
if (!$node) {
- throw new NotFoundException("Version file not accessible by current user");
+ throw new NotFoundException('Version file not accessible by current user');
}
}
} else {
@@ -94,21 +81,28 @@ class ViewOnlyPlugin extends ServerPlugin {
$storage = $node->getStorage();
- if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
+ if (!$storage->instanceOfStorage(ISharedStorage::class)) {
return true;
}
+
// Extract extra permissions
- /** @var \OCA\Files_Sharing\SharedStorage $storage */
+ /** @var ISharedStorage $storage */
$share = $storage->getShare();
-
$attributes = $share->getAttributes();
if ($attributes === null) {
return true;
}
- // Check if read-only and on whether permission can download is both set and disabled.
+ // We have two options here, if download is disabled, but viewing is allowed,
+ // we still allow the GET request to return the file content.
$canDownload = $attributes->getAttribute('permissions', 'download');
- if ($canDownload !== null && !$canDownload) {
+ if (!$share->canSeeContent()) {
+ throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.');
+ }
+
+ // If download is disabled, we disable the COPY and MOVE methods even if the
+ // shareapi_allow_view_without_download is set to true.
+ if ($request->getMethod() !== 'GET' && ($canDownload !== null && !$canDownload)) {
throw new Forbidden('Access to this shared resource has been denied because its download permission is disabled.');
}
} catch (NotFound $e) {
diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php
index dc9afb59653..d7cd46087c3 100644
--- a/apps/dav/lib/Db/Absence.php
+++ b/apps/dav/lib/Db/Absence.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
@@ -46,6 +29,10 @@ use OCP\User\IOutOfOfficeData;
* @method void setStatus(string $status)
* @method string getMessage()
* @method void setMessage(string $message)
+ * @method string getReplacementUserId()
+ * @method void setReplacementUserId(?string $replacementUserId)
+ * @method string getReplacementUserDisplayName()
+ * @method void setReplacementUserDisplayName(?string $replacementUserDisplayName)
*/
class Absence extends Entity implements JsonSerializable {
protected string $userId = '';
@@ -60,17 +47,23 @@ class Absence extends Entity implements JsonSerializable {
protected string $message = '';
+ protected ?string $replacementUserId = null;
+
+ protected ?string $replacementUserDisplayName = null;
+
public function __construct() {
$this->addType('userId', 'string');
$this->addType('firstDay', 'string');
$this->addType('lastDay', 'string');
$this->addType('status', 'string');
$this->addType('message', 'string');
+ $this->addType('replacementUserId', 'string');
+ $this->addType('replacementUserDisplayName', 'string');
}
public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeData {
if ($user->getUID() !== $this->getUserId()) {
- throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID());
+ throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ', got ' . $user->getUID());
}
if ($this->getId() === null) {
throw new Exception('Creating out-of-office data without ID');
@@ -87,6 +80,8 @@ class Absence extends Entity implements JsonSerializable {
$endDate->getTimestamp(),
$this->getStatus(),
$this->getMessage(),
+ $this->getReplacementUserId(),
+ $this->getReplacementUserDisplayName(),
);
}
@@ -97,6 +92,8 @@ class Absence extends Entity implements JsonSerializable {
'lastDay' => $this->lastDay,
'status' => $this->status,
'message' => $this->message,
+ 'replacementUserId' => $this->replacementUserId,
+ 'replacementUserDisplayName' => $this->replacementUserDisplayName,
];
}
}
diff --git a/apps/dav/lib/Db/AbsenceMapper.php b/apps/dav/lib/Db/AbsenceMapper.php
index 7529d04cf10..1214a123236 100644
--- a/apps/dav/lib/Db/AbsenceMapper.php
+++ b/apps/dav/lib/Db/AbsenceMapper.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
diff --git a/apps/dav/lib/Db/Direct.php b/apps/dav/lib/Db/Direct.php
index ca2586ab2e0..4e4a12d225f 100644
--- a/apps/dav/lib/Db/Direct.php
+++ b/apps/dav/lib/Db/Direct.php
@@ -3,29 +3,13 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method string getUserId()
@@ -51,9 +35,9 @@ class Direct extends Entity {
protected $expiration;
public function __construct() {
- $this->addType('userId', 'string');
- $this->addType('fileId', 'int');
- $this->addType('token', 'string');
- $this->addType('expiration', 'int');
+ $this->addType('userId', Types::STRING);
+ $this->addType('fileId', Types::INTEGER);
+ $this->addType('token', Types::STRING);
+ $this->addType('expiration', Types::INTEGER);
}
}
diff --git a/apps/dav/lib/Db/DirectMapper.php b/apps/dav/lib/Db/DirectMapper.php
index c0ed10b97c6..4fedac35b72 100644
--- a/apps/dav/lib/Db/DirectMapper.php
+++ b/apps/dav/lib/Db/DirectMapper.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
@@ -62,6 +45,6 @@ class DirectMapper extends QBMapper {
$qb->expr()->lt('expiration', $qb->createNamedParameter($expiration))
);
- $qb->execute();
+ $qb->executeStatement();
}
}
diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php
index 5234ad852b1..96c5f75ef4f 100644
--- a/apps/dav/lib/Db/Property.php
+++ b/apps/dav/lib/Db/Property.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php
index 6f39d03d1c1..1789194ee7a 100644
--- a/apps/dav/lib/Db/PropertyMapper.php
+++ b/apps/dav/lib/Db/PropertyMapper.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Db;
@@ -54,4 +38,18 @@ class PropertyMapper extends QBMapper {
return $this->findEntities($selectQb);
}
+ /**
+ * @return Property[]
+ */
+ public function findPropertiesByPath(string $userId, string $path): array {
+ $selectQb = $this->db->getQueryBuilder();
+ $selectQb->select('*')
+ ->from(self::TABLE_NAME)
+ ->where(
+ $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)),
+ $selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)),
+ );
+ return $this->findEntities($selectQb);
+ }
+
}
diff --git a/apps/dav/lib/Direct/DirectFile.php b/apps/dav/lib/Direct/DirectFile.php
index b9d1757cfc2..7f41dd65f41 100644
--- a/apps/dav/lib/Direct/DirectFile.php
+++ b/apps/dav/lib/Direct/DirectFile.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Direct;
@@ -36,21 +18,14 @@ use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
class DirectFile implements IFile {
- /** @var Direct */
- private $direct;
-
- /** @var IRootFolder */
- private $rootFolder;
-
/** @var File */
private $file;
- private $eventDispatcher;
-
- public function __construct(Direct $direct, IRootFolder $rootFolder, IEventDispatcher $eventDispatcher) {
- $this->direct = $direct;
- $this->rootFolder = $rootFolder;
- $this->eventDispatcher = $eventDispatcher;
+ public function __construct(
+ private Direct $direct,
+ private IRootFolder $rootFolder,
+ private IEventDispatcher $eventDispatcher,
+ ) {
}
public function put($data) {
@@ -114,7 +89,7 @@ class DirectFile implements IFile {
throw new NotFound();
}
if (!$file instanceof File) {
- throw new Forbidden("direct download not allowed on directories");
+ throw new Forbidden('direct download not allowed on directories');
}
$this->file = $file;
diff --git a/apps/dav/lib/Direct/DirectHome.php b/apps/dav/lib/Direct/DirectHome.php
index 8fc8b555db5..ac411c9b52f 100644
--- a/apps/dav/lib/Direct/DirectHome.php
+++ b/apps/dav/lib/Direct/DirectHome.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Direct;
@@ -40,38 +22,14 @@ use Sabre\DAV\ICollection;
class DirectHome implements ICollection {
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var DirectMapper */
- private $mapper;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var IThrottler */
- private $throttler;
-
- /** @var IRequest */
- private $request;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
public function __construct(
- IRootFolder $rootFolder,
- DirectMapper $mapper,
- ITimeFactory $timeFactory,
- IThrottler $throttler,
- IRequest $request,
- IEventDispatcher $eventDispatcher
+ private IRootFolder $rootFolder,
+ private DirectMapper $mapper,
+ private ITimeFactory $timeFactory,
+ private IThrottler $throttler,
+ private IRequest $request,
+ private IEventDispatcher $eventDispatcher,
) {
- $this->rootFolder = $rootFolder;
- $this->mapper = $mapper;
- $this->timeFactory = $timeFactory;
- $this->throttler = $throttler;
- $this->request = $request;
- $this->eventDispatcher = $eventDispatcher;
}
public function createFile($name, $data = null) {
@@ -95,7 +53,7 @@ class DirectHome implements ICollection {
} catch (DoesNotExistException $e) {
// Since the token space is so huge only throttle on non-existing token
$this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress());
- $this->throttler->sleepDelay($this->request->getRemoteAddress(), 'directlink');
+ $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), 'directlink');
throw new NotFound();
}
diff --git a/apps/dav/lib/Direct/Server.php b/apps/dav/lib/Direct/Server.php
index 0ce5798571d..957f6f99b34 100644
--- a/apps/dav/lib/Direct/Server.php
+++ b/apps/dav/lib/Direct/Server.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Direct;
diff --git a/apps/dav/lib/Direct/ServerFactory.php b/apps/dav/lib/Direct/ServerFactory.php
index f9914a5cb36..473439361c2 100644
--- a/apps/dav/lib/Direct/ServerFactory.php
+++ b/apps/dav/lib/Direct/ServerFactory.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Direct;
@@ -39,17 +20,15 @@ use OCP\L10N\IFactory;
use OCP\Security\Bruteforce\IThrottler;
class ServerFactory {
- /** @var IConfig */
- private $config;
/** @var IL10N */
private $l10n;
- /** @var IEventDispatcher */
- private $eventDispatcher;
- public function __construct(IConfig $config, IFactory $l10nFactory, IEventDispatcher $eventDispatcher) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ IFactory $l10nFactory,
+ private IEventDispatcher $eventDispatcher,
+ ) {
$this->l10n = $l10nFactory->get('dav');
- $this->eventDispatcher = $eventDispatcher;
}
public function createServer(string $baseURI,
diff --git a/apps/dav/lib/Events/AddressBookCreatedEvent.php b/apps/dav/lib/Events/AddressBookCreatedEvent.php
index 396c246289c..1a56bcbf63f 100644
--- a/apps/dav/lib/Events/AddressBookCreatedEvent.php
+++ b/apps/dav/lib/Events/AddressBookCreatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class AddressBookCreatedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
/**
* AddressBookCreatedEvent constructor.
*
@@ -48,11 +25,11 @@ class AddressBookCreatedEvent extends Event {
* @param array $addressBookData
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
}
/**
diff --git a/apps/dav/lib/Events/AddressBookDeletedEvent.php b/apps/dav/lib/Events/AddressBookDeletedEvent.php
index 79785f5f0fa..b1ec4125513 100644
--- a/apps/dav/lib/Events/AddressBookDeletedEvent.php
+++ b/apps/dav/lib/Events/AddressBookDeletedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class AddressBookDeletedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $shares;
-
/**
* AddressBookDeletedEvent constructor.
*
@@ -52,13 +26,12 @@ class AddressBookDeletedEvent extends Event {
* @param array $shares
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $shares) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $shares,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->shares = $shares;
}
/**
diff --git a/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php b/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php
index da3c2701abd..9a574fb548e 100644
--- a/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php
+++ b/apps/dav/lib/Events/AddressBookShareUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,21 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class AddressBookShareUpdatedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $oldShares;
-
- /** @var array */
- private $added;
-
- /** @var array */
- private $removed;
-
/**
* AddressBookShareUpdatedEvent constructor.
*
@@ -60,17 +28,14 @@ class AddressBookShareUpdatedEvent extends Event {
* @param array $removed
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $oldShares,
- array $added,
- array $removed) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $oldShares,
+ private array $added,
+ private array $removed,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->oldShares = $oldShares;
- $this->added = $added;
- $this->removed = $removed;
}
/**
diff --git a/apps/dav/lib/Events/AddressBookUpdatedEvent.php b/apps/dav/lib/Events/AddressBookUpdatedEvent.php
index d651e569467..fe6dc024cd2 100644
--- a/apps/dav/lib/Events/AddressBookUpdatedEvent.php
+++ b/apps/dav/lib/Events/AddressBookUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class AddressBookUpdatedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $mutations;
-
/**
* AddressBookUpdatedEvent constructor.
*
@@ -56,15 +27,13 @@ class AddressBookUpdatedEvent extends Event {
* @param array $mutations
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $shares,
- array $mutations) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $shares,
+ private array $mutations,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->shares = $shares;
- $this->mutations = $mutations;
}
/**
diff --git a/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php b/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php
index ddb79505ac6..a79d730e8ff 100644
--- a/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php
+++ b/apps/dav/lib/Events/BeforeFileDirectDownloadedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -32,11 +15,10 @@ use OCP\Files\File;
* @since 22.0.0
*/
class BeforeFileDirectDownloadedEvent extends Event {
- private $file;
-
- public function __construct(File $file) {
+ public function __construct(
+ private File $file,
+ ) {
parent::__construct();
- $this->file = $file;
}
/**
diff --git a/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php
index 64d9b6dd2c7..ea1c344ed27 100644
--- a/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php
+++ b/apps/dav/lib/Events/CachedCalendarObjectCreatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CachedCalendarObjectCreatedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
/**
* CachedCalendarObjectCreatedEvent constructor.
*
@@ -56,15 +27,13 @@ class CachedCalendarObjectCreatedEvent extends Event {
* @param array $objectData
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData,
- array $shares,
- array $objectData) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ private array $shares,
+ private array $objectData,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
- $this->shares = $shares;
- $this->objectData = $objectData;
}
/**
diff --git a/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php
index 183e8e8bcf9..8f8e55d32e5 100644
--- a/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php
+++ b/apps/dav/lib/Events/CachedCalendarObjectDeletedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CachedCalendarObjectDeletedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
/**
* CachedCalendarObjectDeletedEvent constructor.
*
@@ -56,15 +27,13 @@ class CachedCalendarObjectDeletedEvent extends Event {
* @param array $objectData
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData,
- array $shares,
- array $objectData) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ private array $shares,
+ private array $objectData,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
- $this->shares = $shares;
- $this->objectData = $objectData;
}
/**
diff --git a/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php b/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php
index 62781483def..0adb4164dc9 100644
--- a/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php
+++ b/apps/dav/lib/Events/CachedCalendarObjectUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CachedCalendarObjectUpdatedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
/**
* CachedCalendarObjectUpdatedEvent constructor.
*
@@ -56,15 +27,13 @@ class CachedCalendarObjectUpdatedEvent extends Event {
* @param array $objectData
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData,
- array $shares,
- array $objectData) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ private array $shares,
+ private array $objectData,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
- $this->shares = $shares;
- $this->objectData = $objectData;
}
/**
diff --git a/apps/dav/lib/Events/CalendarCreatedEvent.php b/apps/dav/lib/Events/CalendarCreatedEvent.php
index 649a242a1d2..46d1194914e 100644
--- a/apps/dav/lib/Events/CalendarCreatedEvent.php
+++ b/apps/dav/lib/Events/CalendarCreatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CalendarCreatedEvent extends Event {
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
/**
* CalendarCreatedEvent constructor.
*
@@ -48,11 +25,11 @@ class CalendarCreatedEvent extends Event {
* @param array $calendarData
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
}
/**
diff --git a/apps/dav/lib/Events/CalendarDeletedEvent.php b/apps/dav/lib/Events/CalendarDeletedEvent.php
index 97d522cde7c..c8ab4265b27 100644
--- a/apps/dav/lib/Events/CalendarDeletedEvent.php
+++ b/apps/dav/lib/Events/CalendarDeletedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CalendarDeletedEvent extends Event {
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
/**
* CalendarDeletedEvent constructor.
*
@@ -52,13 +26,12 @@ class CalendarDeletedEvent extends Event {
* @param array $shares
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private array $shares,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
}
/**
diff --git a/apps/dav/lib/Events/CalendarMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php
index 5fb2f48a75c..8bb660a98c6 100644
--- a/apps/dav/lib/Events/CalendarMovedToTrashEvent.php
+++ b/apps/dav/lib/Events/CalendarMovedToTrashEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -32,28 +15,18 @@ use OCP\EventDispatcher\Event;
*/
class CalendarMovedToTrashEvent extends Event {
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
/**
* @param int $calendarId
* @param array $calendarData
* @param array $shares
* @since 22.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private array $shares,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
}
/**
diff --git a/apps/dav/lib/Events/CalendarObjectCreatedEvent.php b/apps/dav/lib/Events/CalendarObjectCreatedEvent.php
deleted file mode 100644
index 123f7fc229f..00000000000
--- a/apps/dav/lib/Events/CalendarObjectCreatedEvent.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * Class CalendarObjectCreatedEvent
- *
- * @package OCA\DAV\Events
- * @since 20.0.0
- */
-class CalendarObjectCreatedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
- /**
- * CalendarObjectCreatedEvent constructor.
- *
- * @param int $calendarId
- * @param array $calendarData
- * @param array $shares
- * @param array $objectData
- * @since 20.0.0
- */
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $objectData) {
- parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 20.0.0
- */
- public function getCalendarId(): int {
- return $this->calendarId;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getCalendarData(): array {
- return $this->calendarData;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getShares(): array {
- return $this->shares;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarObjectDeletedEvent.php b/apps/dav/lib/Events/CalendarObjectDeletedEvent.php
deleted file mode 100644
index 8c5834ff050..00000000000
--- a/apps/dav/lib/Events/CalendarObjectDeletedEvent.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * Class CalendarObjectDeletedEvent
- *
- * @package OCA\DAV\Events
- * @since 20.0.0
- */
-class CalendarObjectDeletedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
- /**
- * CalendarObjectDeletedEvent constructor.
- *
- * @param int $calendarId
- * @param array $calendarData
- * @param array $shares
- * @param array $objectData
- * @since 20.0.0
- */
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $objectData) {
- parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 20.0.0
- */
- public function getCalendarId(): int {
- return $this->calendarId;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getCalendarData(): array {
- return $this->calendarData;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getShares(): array {
- return $this->shares;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarObjectMovedEvent.php b/apps/dav/lib/Events/CalendarObjectMovedEvent.php
deleted file mode 100644
index 402a9adf979..00000000000
--- a/apps/dav/lib/Events/CalendarObjectMovedEvent.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * Class CalendarObjectMovedEvent
- *
- * @package OCA\DAV\Events
- * @since 25.0.0
- */
-class CalendarObjectMovedEvent extends Event {
- private int $sourceCalendarId;
- private array $sourceCalendarData;
- private int $targetCalendarId;
- private array $targetCalendarData;
- private array $sourceShares;
- private array $targetShares;
- private array $objectData;
-
- /**
- * @since 25.0.0
- */
- public function __construct(int $sourceCalendarId,
- array $sourceCalendarData,
- int $targetCalendarId,
- array $targetCalendarData,
- array $sourceShares,
- array $targetShares,
- array $objectData) {
- parent::__construct();
- $this->sourceCalendarId = $sourceCalendarId;
- $this->sourceCalendarData = $sourceCalendarData;
- $this->targetCalendarId = $targetCalendarId;
- $this->targetCalendarData = $targetCalendarData;
- $this->sourceShares = $sourceShares;
- $this->targetShares = $targetShares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 25.0.0
- */
- public function getSourceCalendarId(): int {
- return $this->sourceCalendarId;
- }
-
- /**
- * @return array
- * @since 25.0.0
- */
- public function getSourceCalendarData(): array {
- return $this->sourceCalendarData;
- }
-
- /**
- * @return int
- * @since 25.0.0
- */
- public function getTargetCalendarId(): int {
- return $this->targetCalendarId;
- }
-
- /**
- * @return array
- * @since 25.0.0
- */
- public function getTargetCalendarData(): array {
- return $this->targetCalendarData;
- }
-
- /**
- * @return array
- * @since 25.0.0
- */
- public function getSourceShares(): array {
- return $this->sourceShares;
- }
-
- /**
- * @return array
- * @since 25.0.0
- */
- public function getTargetShares(): array {
- return $this->targetShares;
- }
-
- /**
- * @return array
- * @since 25.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php b/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php
deleted file mode 100644
index b963d1321a4..00000000000
--- a/apps/dav/lib/Events/CalendarObjectMovedToTrashEvent.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * @since 22.0.0
- */
-class CalendarObjectMovedToTrashEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
- /**
- * @param int $calendarId
- * @param array $calendarData
- * @param array $shares
- * @param array $objectData
- * @since 22.0.0
- */
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $objectData) {
- parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 22.0.0
- */
- public function getCalendarId(): int {
- return $this->calendarId;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getCalendarData(): array {
- return $this->calendarData;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getShares(): array {
- return $this->shares;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarObjectRestoredEvent.php b/apps/dav/lib/Events/CalendarObjectRestoredEvent.php
deleted file mode 100644
index 76589725986..00000000000
--- a/apps/dav/lib/Events/CalendarObjectRestoredEvent.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * @since 22.0.0
- */
-class CalendarObjectRestoredEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
- /**
- * @param int $calendarId
- * @param array $calendarData
- * @param array $shares
- * @param array $objectData
- * @since 22.0.0
- */
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $objectData) {
- parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 22.0.0
- */
- public function getCalendarId(): int {
- return $this->calendarId;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getCalendarData(): array {
- return $this->calendarData;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getShares(): array {
- return $this->shares;
- }
-
- /**
- * @return array
- * @since 22.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php b/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php
deleted file mode 100644
index 45d66370137..00000000000
--- a/apps/dav/lib/Events/CalendarObjectUpdatedEvent.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-namespace OCA\DAV\Events;
-
-use OCP\EventDispatcher\Event;
-
-/**
- * Class CalendarObjectUpdatedEvent
- *
- * @package OCA\DAV\Events
- * @since 20.0.0
- */
-class CalendarObjectUpdatedEvent extends Event {
-
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $objectData;
-
- /**
- * CalendarObjectUpdatedEvent constructor.
- *
- * @param int $calendarId
- * @param array $calendarData
- * @param array $shares
- * @param array $objectData
- * @since 20.0.0
- */
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $objectData) {
- parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->objectData = $objectData;
- }
-
- /**
- * @return int
- * @since 20.0.0
- */
- public function getCalendarId(): int {
- return $this->calendarId;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getCalendarData(): array {
- return $this->calendarData;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getShares(): array {
- return $this->shares;
- }
-
- /**
- * @return array
- * @since 20.0.0
- */
- public function getObjectData(): array {
- return $this->objectData;
- }
-}
diff --git a/apps/dav/lib/Events/CalendarPublishedEvent.php b/apps/dav/lib/Events/CalendarPublishedEvent.php
index b92d17901f7..32fb1c36963 100644
--- a/apps/dav/lib/Events/CalendarPublishedEvent.php
+++ b/apps/dav/lib/Events/CalendarPublishedEvent.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,10 +17,6 @@ use OCP\EventDispatcher\Event;
* @since 20.0.0
*/
class CalendarPublishedEvent extends Event {
- private int $calendarId;
- private array $calendarData;
- private string $publicUri;
-
/**
* CalendarPublishedEvent constructor.
*
@@ -47,13 +25,12 @@ class CalendarPublishedEvent extends Event {
* @param string $publicUri
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- string $publicUri) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private string $publicUri,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->publicUri = $publicUri;
}
/**
diff --git a/apps/dav/lib/Events/CalendarRestoredEvent.php b/apps/dav/lib/Events/CalendarRestoredEvent.php
index b44b3607969..f404771dea2 100644
--- a/apps/dav/lib/Events/CalendarRestoredEvent.php
+++ b/apps/dav/lib/Events/CalendarRestoredEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -32,28 +15,18 @@ use OCP\EventDispatcher\Event;
*/
class CalendarRestoredEvent extends Event {
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
/**
* @param int $calendarId
* @param array $calendarData
* @param array $shares
* @since 22.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private array $shares,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
}
/**
diff --git a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php
index ebde62a8be2..0f8b23ad3ac 100644
--- a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php
+++ b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php
@@ -3,74 +3,42 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
+use OCA\DAV\CalDAV\CalDavBackend;
use OCP\EventDispatcher\Event;
-use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
-use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
/**
* Class CalendarShareUpdatedEvent
*
* @package OCA\DAV\Events
* @since 20.0.0
+ *
+ * @psalm-import-type CalendarInfo from CalDavBackend
*/
class CalendarShareUpdatedEvent extends Event {
- private int $calendarId;
-
- /** @var array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } */
- private array $calendarData;
-
- /** @var list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */
- private array $oldShares;
-
- /** @var list<array{href: string, commonName: string, readOnly: bool}> */
- private array $added;
-
- /** @var list<string> */
- private array $removed;
-
/**
* CalendarShareUpdatedEvent constructor.
*
* @param int $calendarId
- * @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string } $calendarData
+ * @psalm-param CalendarInfo $calendarData
+ * @param array $calendarData
* @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $oldShares
* @param list<array{href: string, commonName: string, readOnly: bool}> $added
* @param list<string> $removed
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- array $oldShares,
- array $added,
- array $removed) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private array $oldShares,
+ private array $added,
+ private array $removed,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->oldShares = $oldShares;
- $this->added = $added;
- $this->removed = $removed;
}
/**
@@ -81,7 +49,8 @@ class CalendarShareUpdatedEvent extends Event {
}
/**
- * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }
+ * @psalm-return CalendarInfo
+ * @return array
* @since 20.0.0
*/
public function getCalendarData(): array {
diff --git a/apps/dav/lib/Events/CalendarUnpublishedEvent.php b/apps/dav/lib/Events/CalendarUnpublishedEvent.php
index ede60aeb904..10d1712686d 100644
--- a/apps/dav/lib/Events/CalendarUnpublishedEvent.php
+++ b/apps/dav/lib/Events/CalendarUnpublishedEvent.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,9 +17,6 @@ use OCP\EventDispatcher\Event;
* @since 20.0.0
*/
class CalendarUnpublishedEvent extends Event {
- private int $calendarId;
- private array $calendarData;
-
/**
* CalendarUnpublishedEvent constructor.
*
@@ -45,11 +24,11 @@ class CalendarUnpublishedEvent extends Event {
* @param array $calendarData
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
}
/**
diff --git a/apps/dav/lib/Events/CalendarUpdatedEvent.php b/apps/dav/lib/Events/CalendarUpdatedEvent.php
index c39d22b281c..a603d3152f0 100644
--- a/apps/dav/lib/Events/CalendarUpdatedEvent.php
+++ b/apps/dav/lib/Events/CalendarUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CalendarUpdatedEvent extends Event {
- /** @var int */
- private $calendarId;
-
- /** @var array */
- private $calendarData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $mutations;
-
/**
* CalendarUpdatedEvent constructor.
*
@@ -56,15 +27,13 @@ class CalendarUpdatedEvent extends Event {
* @param array $mutations
* @since 20.0.0
*/
- public function __construct(int $calendarId,
- array $calendarData,
- array $shares,
- array $mutations) {
+ public function __construct(
+ private int $calendarId,
+ private array $calendarData,
+ private array $shares,
+ private array $mutations,
+ ) {
parent::__construct();
- $this->calendarId = $calendarId;
- $this->calendarData = $calendarData;
- $this->shares = $shares;
- $this->mutations = $mutations;
}
/**
diff --git a/apps/dav/lib/Events/CardCreatedEvent.php b/apps/dav/lib/Events/CardCreatedEvent.php
index 138cccd6862..5a66d73e707 100644
--- a/apps/dav/lib/Events/CardCreatedEvent.php
+++ b/apps/dav/lib/Events/CardCreatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CardCreatedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $cardData;
-
/**
* CardCreatedEvent constructor.
*
@@ -56,15 +27,13 @@ class CardCreatedEvent extends Event {
* @param array $cardData
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $shares,
- array $cardData) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $shares,
+ private array $cardData,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->shares = $shares;
- $this->cardData = $cardData;
}
/**
diff --git a/apps/dav/lib/Events/CardDeletedEvent.php b/apps/dav/lib/Events/CardDeletedEvent.php
index e0a92a2076a..237ffa7d623 100644
--- a/apps/dav/lib/Events/CardDeletedEvent.php
+++ b/apps/dav/lib/Events/CardDeletedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CardDeletedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $cardData;
-
/**
* CardDeletedEvent constructor.
*
@@ -56,15 +27,13 @@ class CardDeletedEvent extends Event {
* @param array $cardData
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $shares,
- array $cardData) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $shares,
+ private array $cardData,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->shares = $shares;
- $this->cardData = $cardData;
}
/**
diff --git a/apps/dav/lib/Events/CardMovedEvent.php b/apps/dav/lib/Events/CardMovedEvent.php
index 38a66ed08b6..be69a046537 100644
--- a/apps/dav/lib/Events/CardMovedEvent.php
+++ b/apps/dav/lib/Events/CardMovedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023, Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -34,32 +17,19 @@ use OCP\EventDispatcher\Event;
* @since 27.0.0
*/
class CardMovedEvent extends Event {
- private int $sourceAddressBookId;
- private array $sourceAddressBookData;
- private int $targetAddressBookId;
- private array $targetAddressBookData;
- private array $sourceShares;
- private array $targetShares;
- private array $objectData;
-
/**
* @since 27.0.0
*/
- public function __construct(int $sourceAddressBookId,
- array $sourceAddressBookData,
- int $targetAddressBookId,
- array $targetAddressBookData,
- array $sourceShares,
- array $targetShares,
- array $objectData) {
+ public function __construct(
+ private int $sourceAddressBookId,
+ private array $sourceAddressBookData,
+ private int $targetAddressBookId,
+ private array $targetAddressBookData,
+ private array $sourceShares,
+ private array $targetShares,
+ private array $objectData,
+ ) {
parent::__construct();
- $this->sourceAddressBookId = $sourceAddressBookId;
- $this->sourceAddressBookData = $sourceAddressBookData;
- $this->targetAddressBookId = $targetAddressBookId;
- $this->targetAddressBookData = $targetAddressBookData;
- $this->sourceShares = $sourceShares;
- $this->targetShares = $targetShares;
- $this->objectData = $objectData;
}
/**
diff --git a/apps/dav/lib/Events/CardUpdatedEvent.php b/apps/dav/lib/Events/CardUpdatedEvent.php
index 40f28713ce6..10fc5b74594 100644
--- a/apps/dav/lib/Events/CardUpdatedEvent.php
+++ b/apps/dav/lib/Events/CardUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class CardUpdatedEvent extends Event {
- /** @var int */
- private $addressBookId;
-
- /** @var array */
- private $addressBookData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $cardData;
-
/**
* CardUpdatedEvent constructor.
*
@@ -56,15 +27,13 @@ class CardUpdatedEvent extends Event {
* @param array $cardData
* @since 20.0.0
*/
- public function __construct(int $addressBookId,
- array $addressBookData,
- array $shares,
- array $cardData) {
+ public function __construct(
+ private int $addressBookId,
+ private array $addressBookData,
+ private array $shares,
+ private array $cardData,
+ ) {
parent::__construct();
- $this->addressBookId = $addressBookId;
- $this->addressBookData = $addressBookData;
- $this->shares = $shares;
- $this->cardData = $cardData;
}
/**
diff --git a/apps/dav/lib/Events/SabrePluginAddEvent.php b/apps/dav/lib/Events/SabrePluginAddEvent.php
index 3bff756e2a1..866fae2e224 100644
--- a/apps/dav/lib/Events/SabrePluginAddEvent.php
+++ b/apps/dav/lib/Events/SabrePluginAddEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -36,15 +19,13 @@ use Sabre\DAV\Server;
*/
class SabrePluginAddEvent extends Event {
- /** @var Server */
- private $server;
-
/**
* @since 28.0.0
*/
- public function __construct(Server $server) {
+ public function __construct(
+ private Server $server,
+ ) {
parent::__construct();
- $this->server = $server;
}
/**
diff --git a/apps/dav/lib/Events/SabrePluginAuthInitEvent.php b/apps/dav/lib/Events/SabrePluginAuthInitEvent.php
index ea1bd95a0f7..d3d93770c74 100644
--- a/apps/dav/lib/Events/SabrePluginAuthInitEvent.php
+++ b/apps/dav/lib/Events/SabrePluginAuthInitEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -36,14 +19,12 @@ use Sabre\DAV\Server;
*/
class SabrePluginAuthInitEvent extends Event {
- /** @var Server */
- private $server;
-
/**
* @since 20.0.0
*/
- public function __construct(Server $server) {
- $this->server = $server;
+ public function __construct(
+ private Server $server,
+ ) {
}
/**
diff --git a/apps/dav/lib/Events/SubscriptionCreatedEvent.php b/apps/dav/lib/Events/SubscriptionCreatedEvent.php
index 9932e70c4c8..433b6db59b0 100644
--- a/apps/dav/lib/Events/SubscriptionCreatedEvent.php
+++ b/apps/dav/lib/Events/SubscriptionCreatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,12 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class SubscriptionCreatedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
/**
* SubscriptionCreatedEvent constructor.
*
@@ -48,11 +25,11 @@ class SubscriptionCreatedEvent extends Event {
* @param array $subscriptionData
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
}
/**
diff --git a/apps/dav/lib/Events/SubscriptionDeletedEvent.php b/apps/dav/lib/Events/SubscriptionDeletedEvent.php
index 8aa36f4584b..58d47ae98a4 100644
--- a/apps/dav/lib/Events/SubscriptionDeletedEvent.php
+++ b/apps/dav/lib/Events/SubscriptionDeletedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,15 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class SubscriptionDeletedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
- /** @var array */
- private $shares;
-
/**
* SubscriptionDeletedEvent constructor.
*
@@ -52,13 +26,12 @@ class SubscriptionDeletedEvent extends Event {
* @param array $shares
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData,
- array $shares) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ private array $shares,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
- $this->shares = $shares;
}
/**
diff --git a/apps/dav/lib/Events/SubscriptionUpdatedEvent.php b/apps/dav/lib/Events/SubscriptionUpdatedEvent.php
index eb90177a00d..1a1b2804663 100644
--- a/apps/dav/lib/Events/SubscriptionUpdatedEvent.php
+++ b/apps/dav/lib/Events/SubscriptionUpdatedEvent.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Events;
@@ -35,18 +18,6 @@ use OCP\EventDispatcher\Event;
*/
class SubscriptionUpdatedEvent extends Event {
- /** @var int */
- private $subscriptionId;
-
- /** @var array */
- private $subscriptionData;
-
- /** @var array */
- private $shares;
-
- /** @var array */
- private $mutations;
-
/**
* SubscriptionUpdatedEvent constructor.
*
@@ -56,15 +27,13 @@ class SubscriptionUpdatedEvent extends Event {
* @param array $mutations
* @since 20.0.0
*/
- public function __construct(int $subscriptionId,
- array $subscriptionData,
- array $shares,
- array $mutations) {
+ public function __construct(
+ private int $subscriptionId,
+ private array $subscriptionData,
+ private array $shares,
+ private array $mutations,
+ ) {
parent::__construct();
- $this->subscriptionId = $subscriptionId;
- $this->subscriptionData = $subscriptionData;
- $this->shares = $shares;
- $this->mutations = $mutations;
}
/**
diff --git a/apps/dav/lib/ExampleContentFiles/exampleContact.vcf b/apps/dav/lib/ExampleContentFiles/exampleContact.vcf
new file mode 100644
index 00000000000..c58c949d0db
--- /dev/null
+++ b/apps/dav/lib/ExampleContentFiles/exampleContact.vcf
@@ -0,0 +1,3555 @@
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 4.5.6//EN
+UID:cffff367-4580-4e01-8b6d-f91e95ce7e92
+FN:Leon Green
+ADR;TYPE=HOME:;;123 Street Street;City;State;;Country
+EMAIL;TYPE=HOME:leon@example.com
+TEL;TYPE=HOME,VOICE:+999999999999
+PHOTO;ENCODING=b;TYPE=PNG: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
+ ORK5CYII=
+PHOTO;VALUE=URI:
+PHOTO;VALUE=URI:
+TITLE:Manager
+ORG:Company
+BDAY;VALUE=DATE:20000101
+URL;VALUE=URI:www.nextcloud.com
+REV;VALUE=DATE-AND-OR-TIME:20250108T160752Z
+END:VCARD
diff --git a/apps/dav/lib/Exception/ExampleEventException.php b/apps/dav/lib/Exception/ExampleEventException.php
new file mode 100644
index 00000000000..2d77cc443cb
--- /dev/null
+++ b/apps/dav/lib/Exception/ExampleEventException.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Exception;
+
+class ExampleEventException extends \Exception {
+}
diff --git a/apps/dav/lib/Exception/ServerMaintenanceMode.php b/apps/dav/lib/Exception/ServerMaintenanceMode.php
index 9dad9f2d4d1..8f621588fdc 100644
--- a/apps/dav/lib/Exception/ServerMaintenanceMode.php
+++ b/apps/dav/lib/Exception/ServerMaintenanceMode.php
@@ -2,25 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Exception;
diff --git a/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php
index 255a06578ac..c6b7f8564c5 100644
--- a/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php
+++ b/apps/dav/lib/Exception/UnsupportedLimitOnInitialSyncException.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2019 Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Exception;
diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
index eccae8afdd5..85ed975a409 100644
--- a/apps/dav/lib/Files/BrowserErrorPagePlugin.php
+++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php
@@ -1,33 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Files;
use OC\AppFramework\Http\Request;
-use OC_Template;
use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
+use OCP\Security\Bruteforce\MaxDelayReached;
+use OCP\Template\ITemplateManager;
use Sabre\DAV\Exception;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
@@ -76,6 +61,9 @@ class BrowserErrorPagePlugin extends ServerPlugin {
if ($ex instanceof Exception) {
$httpCode = $ex->getHTTPCode();
$headers = $ex->getHTTPHeaders($this->server);
+ } elseif ($ex instanceof MaxDelayReached) {
+ $httpCode = 429;
+ $headers = [];
} else {
$httpCode = 500;
$headers = [];
@@ -94,14 +82,14 @@ class BrowserErrorPagePlugin extends ServerPlugin {
* @return bool|string
*/
public function generateBody(int $httpCode) {
- $request = \OC::$server->getRequest();
+ $request = \OCP\Server::get(IRequest::class);
$templateName = 'exception';
- if ($httpCode === 403 || $httpCode === 404) {
+ if ($httpCode === 403 || $httpCode === 404 || $httpCode === 429) {
$templateName = (string)$httpCode;
}
- $content = new OC_Template('core', $templateName, 'guest');
+ $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST);
$content->assign('title', $this->server->httpResponse->getStatusText());
$content->assign('remoteAddr', $request->getRemoteAddress());
$content->assign('requestID', $request->getId());
diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php
index 8b87ae624ba..eb548bbd55c 100644
--- a/apps/dav/lib/Files/FileSearchBackend.php
+++ b/apps/dav/lib/Files/FileSearchBackend.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
- *
- * @author Christian <16852529+cviereck@users.noreply.github.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files;
@@ -34,7 +14,9 @@ use OC\Files\Storage\Wrapper\Jail;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Connector\Sabre\Server;
use OCA\DAV\Connector\Sabre\TagsPlugin;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Folder;
@@ -64,6 +46,7 @@ class FileSearchBackend implements ISearchBackend {
public const OPERATOR_LIMIT = 100;
public function __construct(
+ private Server $server,
private CachingTree $tree,
private IUser $user,
private IRootFolder $rootFolder,
@@ -153,6 +136,7 @@ class FileSearchBackend implements ISearchBackend {
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
+ $this->server->emit('preloadProperties', [$nodes, $requestProperties]);
}
private function getFolderForPath(?string $path = null): Folder {
@@ -227,9 +211,9 @@ class FileSearchBackend implements ISearchBackend {
/** @var SearchResult[] $nodes */
$nodes = array_map(function (Node $node) {
if ($node instanceof Folder) {
- $davNode = new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager);
+ $davNode = new Directory($this->view, $node, $this->tree, $this->shareManager);
} else {
- $davNode = new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager);
+ $davNode = new File($this->view, $node, $this->shareManager);
}
$path = $this->getHrefForNode($node);
$this->tree->cacheNode($davNode, $path);
@@ -442,10 +426,16 @@ class FileSearchBackend implements ISearchBackend {
$field = $this->mapPropertyNameToColumn($property);
}
+ try {
+ $castedValue = $this->castValue($property, $value ?? '');
+ } catch (\Error $e) {
+ throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
+ }
+
return new SearchComparison(
$trimmedType,
$field,
- $this->castValue($property, $value ?? ''),
+ $castedValue,
$extra ?? ''
);
diff --git a/apps/dav/lib/Files/FilesHome.php b/apps/dav/lib/Files/FilesHome.php
index 0a781b5589d..f8aa82cdcc9 100644
--- a/apps/dav/lib/Files/FilesHome.php
+++ b/apps/dav/lib/Files/FilesHome.php
@@ -1,30 +1,13 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Files;
+use OC\Files\Filesystem;
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\FileInfo;
use Sabre\DAV\Exception\Forbidden;
@@ -32,19 +15,16 @@ use Sabre\DAV\Exception\Forbidden;
class FilesHome extends Directory {
/**
- * @var array
- */
- private $principalInfo;
-
- /**
* FilesHome constructor.
*
* @param array $principalInfo
* @param FileInfo $userFolder
*/
- public function __construct($principalInfo, FileInfo $userFolder) {
- $this->principalInfo = $principalInfo;
- $view = \OC\Files\Filesystem::getView();
+ public function __construct(
+ private $principalInfo,
+ FileInfo $userFolder,
+ ) {
+ $view = Filesystem::getView();
parent::__construct($view, $userFolder);
}
diff --git a/apps/dav/lib/Files/LazySearchBackend.php b/apps/dav/lib/Files/LazySearchBackend.php
index ccd6fde14e1..6ba539ddd87 100644
--- a/apps/dav/lib/Files/LazySearchBackend.php
+++ b/apps/dav/lib/Files/LazySearchBackend.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files;
diff --git a/apps/dav/lib/Files/RootCollection.php b/apps/dav/lib/Files/RootCollection.php
index 15498ec26ec..a11bea72c59 100644
--- a/apps/dav/lib/Files/RootCollection.php
+++ b/apps/dav/lib/Files/RootCollection.php
@@ -1,31 +1,15 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Files;
use OCP\Files\FileInfo;
+use OCP\IUserSession;
+use OCP\Server;
use Sabre\DAV\INode;
use Sabre\DAV\SimpleCollection;
use Sabre\DAVACL\AbstractPrincipalCollection;
@@ -44,7 +28,7 @@ class RootCollection extends AbstractPrincipalCollection {
*/
public function getChildForPrincipal(array $principalInfo) {
[,$name] = \Sabre\Uri\split($principalInfo['uri']);
- $user = \OC::$server->getUserSession()->getUser();
+ $user = Server::get(IUserSession::class)->getUser();
if (is_null($user) || $name !== $user->getUID()) {
// a user is only allowed to see their own home contents, so in case another collection
// is accessed, we return a simple empty collection for now
diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
index 3ac541bbfd9..a3dbd32ce6b 100644
--- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
+++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
@@ -1,30 +1,15 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Files\Sharing;
-use OC\Files\View;
+use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
+use OCP\Share\IShare;
+use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -35,51 +20,180 @@ use Sabre\HTTP\ResponseInterface;
*/
class FilesDropPlugin extends ServerPlugin {
- /** @var View */
- private $view;
-
- /** @var bool */
- private $enabled = false;
+ private ?IShare $share = null;
+ private bool $enabled = false;
- /**
- * @param View $view
- */
- public function setView($view) {
- $this->view = $view;
+ public function setShare(IShare $share): void {
+ $this->share = $share;
}
- public function enable() {
+ public function enable(): void {
$this->enabled = true;
}
-
/**
* This initializes the plugin.
- *
- * @param \Sabre\DAV\Server $server Sabre server
- *
- * @return void
- * @throws MethodNotAllowed
+ * It is ONLY initialized by the server on a file drop request.
*/
- public function initialize(\Sabre\DAV\Server $server) {
+ public function initialize(\Sabre\DAV\Server $server): void {
$server->on('beforeMethod:*', [$this, 'beforeMethod'], 999);
+ $server->on('method:MKCOL', [$this, 'onMkcol']);
$this->enabled = false;
}
+ public function onMkcol(RequestInterface $request, ResponseInterface $response) {
+ if (!$this->enabled || $this->share === null) {
+ return;
+ }
+
+ $node = $this->share->getNode();
+ if (!($node instanceof Folder)) {
+ return;
+ }
+
+ // If this is a folder creation request we need
+ // to fake a success so we can pretend every
+ // folder now exists.
+ $response->setStatus(201);
+ return false;
+ }
+
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
- if (!$this->enabled) {
+ if (!$this->enabled || $this->share === null) {
return;
}
+ $node = $this->share->getNode();
+ if (!($node instanceof Folder)) {
+ return;
+ }
+
+ // Retrieve the nickname from the request
+ $nickname = $request->hasHeader('X-NC-Nickname')
+ ? trim(urldecode($request->getHeader('X-NC-Nickname')))
+ : null;
+
if ($request->getMethod() !== 'PUT') {
- throw new MethodNotAllowed('Only PUT is allowed on files drop');
+ // If uploading subfolders we need to ensure they get created
+ // within the nickname folder
+ if ($request->getMethod() === 'MKCOL') {
+ if (!$nickname) {
+ throw new BadRequest('A nickname header is required when uploading subfolders');
+ }
+ } else {
+ throw new MethodNotAllowed('Only PUT is allowed on files drop');
+ }
+ }
+
+ // If this is a folder creation request
+ // let's stop there and let the onMkcol handle it
+ if ($request->getMethod() === 'MKCOL') {
+ return;
+ }
+
+ // Now if we create a file, we need to create the
+ // full path along the way. We'll only handle conflict
+ // resolution on file conflicts, but not on folders.
+
+ // e.g files/dCP8yn3N86EK9sL/Folder/image.jpg
+ $path = $request->getPath();
+ $token = $this->share->getToken();
+
+ // e.g files/dCP8yn3N86EK9sL
+ $rootPath = substr($path, 0, strpos($path, $token) + strlen($token));
+ // e.g /Folder/image.jpg
+ $relativePath = substr($path, strlen($rootPath));
+ $isRootUpload = substr_count($relativePath, '/') === 1;
+
+ // Extract the attributes for the file request
+ $isFileRequest = false;
+ $attributes = $this->share->getAttributes();
+ if ($attributes !== null) {
+ $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
+ }
+
+ // We need a valid nickname for file requests
+ if ($isFileRequest && !$nickname) {
+ throw new BadRequest('A nickname header is required for file requests');
+ }
+
+ // We're only allowing the upload of
+ // long path with subfolders if a nickname is set.
+ // This prevents confusion when uploading files and help
+ // classify them by uploaders.
+ if (!$nickname && !$isRootUpload) {
+ throw new BadRequest('A nickname header is required when uploading subfolders');
}
- $path = explode('/', $request->getPath());
- $path = array_pop($path);
+ if ($nickname) {
+ try {
+ $node->verifyPath($nickname);
+ } catch (\Exception $e) {
+ // If the path is not valid, we throw an exception
+ throw new BadRequest('Invalid nickname: ' . $nickname);
+ }
- $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view);
- $url = $request->getBaseUrl() . $newName;
+ // Forbid nicknames starting with a dot
+ if (str_starts_with($nickname, '.')) {
+ throw new BadRequest('Invalid nickname: ' . $nickname);
+ }
+
+ // If we have a nickname, let's put
+ // all files in the subfolder
+ $relativePath = '/' . $nickname . '/' . $relativePath;
+ $relativePath = str_replace('//', '/', $relativePath);
+ }
+
+ // Create the folders along the way
+ $folder = $node;
+ $pathSegments = $this->getPathSegments(dirname($relativePath));
+ foreach ($pathSegments as $pathSegment) {
+ if ($pathSegment === '') {
+ continue;
+ }
+
+ try {
+ // get the current folder
+ $currentFolder = $folder->get($pathSegment);
+ // check target is a folder
+ if ($currentFolder instanceof Folder) {
+ $folder = $currentFolder;
+ } else {
+ // otherwise look in the parent folder if we already create an unique folder name
+ foreach ($folder->getDirectoryListing() as $child) {
+ // we look for folders which match "NAME (SUFFIX)"
+ if ($child instanceof Folder && str_starts_with($child->getName(), $pathSegment)) {
+ $suffix = substr($child->getName(), strlen($pathSegment));
+ if (preg_match('/^ \(\d+\)$/', $suffix)) {
+ // we found the unique folder name and can use it
+ $folder = $child;
+ break;
+ }
+ }
+ }
+ // no folder found so we need to create a new unique folder name
+ if (!isset($child) || $child !== $folder) {
+ $folder = $folder->newFolder($folder->getNonExistingName($pathSegment));
+ }
+ }
+ } catch (NotFoundException) {
+ // the folder does simply not exist so we create it
+ $folder = $folder->newFolder($pathSegment);
+ }
+ }
+
+ // Finally handle conflicts on the end files
+ $uniqueName = $folder->getNonExistingName(basename($relativePath));
+ $relativePath = substr($folder->getPath(), strlen($node->getPath()));
+ $path = '/files/' . $token . '/' . $relativePath . '/' . $uniqueName;
+ $url = rtrim($request->getBaseUrl(), '/') . str_replace('//', '/', $path);
$request->setUrl($url);
}
+
+ private function getPathSegments(string $path): array {
+ // Normalize slashes and remove trailing slash
+ $path = trim(str_replace('\\', '/', $path), '/');
+
+ return explode('/', $path);
+ }
}
diff --git a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php
index 94cd6d29c6c..38a45b3fc37 100644
--- a/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php
+++ b/apps/dav/lib/Files/Sharing/PublicLinkCheckPlugin.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Files\Sharing;
@@ -57,7 +41,7 @@ class PublicLinkCheckPlugin extends ServerPlugin {
}
public function beforeMethod(RequestInterface $request, ResponseInterface $response) {
- // verify that the owner didn't have his share permissions revoked
+ // verify that the owner didn't have their share permissions revoked
if ($this->fileInfo && !$this->fileInfo->isShareable()) {
throw new NotFound();
}
diff --git a/apps/dav/lib/Files/Sharing/RootCollection.php b/apps/dav/lib/Files/Sharing/RootCollection.php
new file mode 100644
index 00000000000..dd585fbb59b
--- /dev/null
+++ b/apps/dav/lib/Files/Sharing/RootCollection.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Files\Sharing;
+
+use Sabre\DAV\INode;
+use Sabre\DAVACL\AbstractPrincipalCollection;
+use Sabre\DAVACL\PrincipalBackend\BackendInterface;
+
+class RootCollection extends AbstractPrincipalCollection {
+ public function __construct(
+ private INode $root,
+ BackendInterface $principalBackend,
+ string $principalPrefix = 'principals',
+ ) {
+ parent::__construct($principalBackend, $principalPrefix);
+ }
+
+ public function getChildForPrincipal(array $principalInfo): INode {
+ return $this->root;
+ }
+
+ public function getName() {
+ return 'files';
+ }
+}
diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php
deleted file mode 100644
index 46cf9621f47..00000000000
--- a/apps/dav/lib/HookManager.php
+++ /dev/null
@@ -1,194 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-namespace OCA\DAV;
-
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCA\DAV\CardDAV\CardDavBackend;
-use OCA\DAV\CardDAV\SyncService;
-use OCP\Defaults;
-use OCP\IUser;
-use OCP\IUserManager;
-use OCP\Util;
-use Psr\Log\LoggerInterface;
-
-class HookManager {
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var SyncService */
- private $syncService;
-
- /** @var IUser[] */
- private $usersToDelete = [];
-
- /** @var CalDavBackend */
- private $calDav;
-
- /** @var CardDavBackend */
- private $cardDav;
-
- /** @var array */
- private $calendarsToDelete = [];
-
- /** @var array */
- private $subscriptionsToDelete = [];
-
- /** @var array */
- private $addressBooksToDelete = [];
-
- /** @var Defaults */
- private $themingDefaults;
-
- public function __construct(IUserManager $userManager,
- SyncService $syncService,
- CalDavBackend $calDav,
- CardDavBackend $cardDav,
- Defaults $themingDefaults) {
- $this->userManager = $userManager;
- $this->syncService = $syncService;
- $this->calDav = $calDav;
- $this->cardDav = $cardDav;
- $this->themingDefaults = $themingDefaults;
- }
-
- public function setup() {
- Util::connectHook('OC_User',
- 'post_createUser',
- $this,
- 'postCreateUser');
- \OC::$server->getUserManager()->listen('\OC\User', 'assignedUserId', function ($uid) {
- $this->postCreateUser(['uid' => $uid]);
- });
- Util::connectHook('OC_User',
- 'pre_deleteUser',
- $this,
- 'preDeleteUser');
- \OC::$server->getUserManager()->listen('\OC\User', 'preUnassignedUserId', [$this, 'preUnassignedUserId']);
- Util::connectHook('OC_User',
- 'post_deleteUser',
- $this,
- 'postDeleteUser');
- \OC::$server->getUserManager()->listen('\OC\User', 'postUnassignedUserId', function ($uid) {
- $this->postDeleteUser(['uid' => $uid]);
- });
- \OC::$server->getUserManager()->listen('\OC\User', 'postUnassignedUserId', [$this, 'postUnassignedUserId']);
- Util::connectHook('OC_User', 'changeUser', $this, 'changeUser');
- }
-
- public function postCreateUser($params) {
- $user = $this->userManager->get($params['uid']);
- if ($user instanceof IUser) {
- $this->syncService->updateUser($user);
- }
- }
-
- public function preDeleteUser($params) {
- $uid = $params['uid'];
- $userPrincipalUri = 'principals/users/' . $uid;
- $this->usersToDelete[$uid] = $this->userManager->get($uid);
- $this->calendarsToDelete = $this->calDav->getUsersOwnCalendars($userPrincipalUri);
- $this->subscriptionsToDelete = $this->calDav->getSubscriptionsForUser($userPrincipalUri);
- $this->addressBooksToDelete = $this->cardDav->getUsersOwnAddressBooks($userPrincipalUri);
- }
-
- public function preUnassignedUserId($uid) {
- $this->usersToDelete[$uid] = $this->userManager->get($uid);
- }
-
- public function postDeleteUser($params) {
- $uid = $params['uid'];
- if (isset($this->usersToDelete[$uid])) {
- $this->syncService->deleteUser($this->usersToDelete[$uid]);
- }
-
- foreach ($this->calendarsToDelete as $calendar) {
- $this->calDav->deleteCalendar(
- $calendar['id'],
- true // Make sure the data doesn't go into the trashbin, a new user with the same UID would later see it otherwise
- );
- }
-
- foreach ($this->subscriptionsToDelete as $subscription) {
- $this->calDav->deleteSubscription(
- $subscription['id'],
- );
- }
- $this->calDav->deleteAllSharesByUser('principals/users/' . $uid);
-
- foreach ($this->addressBooksToDelete as $addressBook) {
- $this->cardDav->deleteAddressBook($addressBook['id']);
- }
- }
-
- public function postUnassignedUserId($uid) {
- if (isset($this->usersToDelete[$uid])) {
- $this->syncService->deleteUser($this->usersToDelete[$uid]);
- }
- }
-
- public function changeUser($params) {
- $user = $params['user'];
- $feature = $params['feature'];
- // This case is already covered by the account manager firing up a signal
- // later on
- if ($feature !== 'eMailAddress' && $feature !== 'displayName') {
- $this->syncService->updateUser($user);
- }
- }
-
- /**
- * @return void
- */
- public function firstLogin(?IUser $user = null) {
- if (!is_null($user)) {
- $principal = 'principals/users/' . $user->getUID();
- if ($this->calDav->getCalendarsForUserCount($principal) === 0) {
- try {
- $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [
- '{DAV:}displayname' => CalDavBackend::PERSONAL_CALENDAR_NAME,
- '{http://apple.com/ns/ical/}calendar-color' => $this->themingDefaults->getColorPrimary(),
- 'components' => 'VEVENT'
- ]);
- } catch (\Exception $e) {
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
- }
- }
- if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) {
- try {
- $this->cardDav->createAddressBook($principal, CardDavBackend::PERSONAL_ADDRESSBOOK_URI, [
- '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME,
- ]);
- } catch (\Exception $e) {
- \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
- }
- }
- }
- }
-}
diff --git a/apps/dav/lib/Listener/ActivityUpdaterListener.php b/apps/dav/lib/Listener/ActivityUpdaterListener.php
index 3f958965f06..f291e424c41 100644
--- a/apps/dav/lib/Listener/ActivityUpdaterListener.php
+++ b/apps/dav/lib/Listener/ActivityUpdaterListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -30,14 +13,14 @@ use OCA\DAV\DAV\Sharing\Plugin;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectDeletedEvent;
-use OCA\DAV\Events\CalendarObjectMovedEvent;
-use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectRestoredEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarUpdatedEvent;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent;
+use OCP\Calendar\Events\CalendarObjectRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
@@ -47,16 +30,10 @@ use function sprintf;
/** @template-implements IEventListener<CalendarCreatedEvent|CalendarUpdatedEvent|CalendarMovedToTrashEvent|CalendarRestoredEvent|CalendarDeletedEvent|CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectMovedEvent|CalendarObjectMovedToTrashEvent|CalendarObjectRestoredEvent|CalendarObjectDeletedEvent> */
class ActivityUpdaterListener implements IEventListener {
- /** @var ActivityBackend */
- private $activityBackend;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(ActivityBackend $activityBackend,
- LoggerInterface $logger) {
- $this->activityBackend = $activityBackend;
- $this->logger = $logger;
+ public function __construct(
+ private ActivityBackend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/AddMissingIndicesListener.php b/apps/dav/lib/Listener/AddMissingIndicesListener.php
new file mode 100644
index 00000000000..d3a1cf4b224
--- /dev/null
+++ b/apps/dav/lib/Listener/AddMissingIndicesListener.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCP\DB\Events\AddMissingIndicesEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/**
+ * @template-implements IEventListener<Event|AddMissingIndicesEvent>
+ */
+class AddMissingIndicesListener implements IEventListener {
+
+ public function handle(Event $event): void {
+ if (!($event instanceof AddMissingIndicesEvent)) {
+ return;
+ }
+ $event->addMissingIndex(
+ 'dav_shares',
+ 'dav_shares_resourceid_type',
+ ['resourceid', 'type']
+ );
+ $event->addMissingIndex(
+ 'dav_shares',
+ 'dav_shares_resourceid_access',
+ ['resourceid', 'access']
+ );
+ $event->addMissingIndex(
+ 'calendarobjects',
+ 'calobjects_by_uid_index',
+ ['calendarid', 'calendartype', 'uid']
+ );
+ }
+
+}
diff --git a/apps/dav/lib/Listener/AddressbookListener.php b/apps/dav/lib/Listener/AddressbookListener.php
index 6c348a25b59..4e38ce50dfd 100644
--- a/apps/dav/lib/Listener/AddressbookListener.php
+++ b/apps/dav/lib/Listener/AddressbookListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -38,16 +21,10 @@ use function sprintf;
/** @template-implements IEventListener<AddressBookCreatedEvent|AddressBookUpdatedEvent|AddressBookDeletedEvent|AddressBookShareUpdatedEvent> */
class AddressbookListener implements IEventListener {
- /** @var ActivityBackend */
- private $activityBackend;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(ActivityBackend $activityBackend,
- LoggerInterface $logger) {
- $this->activityBackend = $activityBackend;
- $this->logger = $logger;
+ public function __construct(
+ private ActivityBackend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/BirthdayListener.php b/apps/dav/lib/Listener/BirthdayListener.php
index a315cfcc74c..3a464d668f9 100644
--- a/apps/dav/lib/Listener/BirthdayListener.php
+++ b/apps/dav/lib/Listener/BirthdayListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -34,10 +17,9 @@ use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<CardCreatedEvent|CardUpdatedEvent|CardDeletedEvent> */
class BirthdayListener implements IEventListener {
- private BirthdayService $birthdayService;
-
- public function __construct(BirthdayService $birthdayService) {
- $this->birthdayService = $birthdayService;
+ public function __construct(
+ private BirthdayService $birthdayService,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/CalendarContactInteractionListener.php b/apps/dav/lib/Listener/CalendarContactInteractionListener.php
index 5e23891de3d..a7f00e452c4 100644
--- a/apps/dav/lib/Listener/CalendarContactInteractionListener.php
+++ b/apps/dav/lib/Listener/CalendarContactInteractionListener.php
@@ -3,32 +3,15 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
use OCA\DAV\Connector\Sabre\Principal;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\Contacts\Events\ContactInteractedWithEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
@@ -49,31 +32,13 @@ use function substr;
class CalendarContactInteractionListener implements IEventListener {
private const URI_USERS = 'principals/users/';
- /** @var IEventDispatcher */
- private $dispatcher;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var Principal */
- private $principalConnector;
-
- /** @var IMailer */
- private $mailer;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(IEventDispatcher $dispatcher,
- IUserSession $userSession,
- Principal $principalConnector,
- IMailer $mailer,
- LoggerInterface $logger) {
- $this->dispatcher = $dispatcher;
- $this->userSession = $userSession;
- $this->principalConnector = $principalConnector;
- $this->mailer = $mailer;
- $this->logger = $logger;
+ public function __construct(
+ private IEventDispatcher $dispatcher,
+ private IUserSession $userSession,
+ private Principal $principalConnector,
+ private IMailer $mailer,
+ private LoggerInterface $logger,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php b/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php
index 39f33154603..0cfc435eb8c 100644
--- a/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php
+++ b/apps/dav/lib/Listener/CalendarDeletionDefaultUpdaterListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -37,16 +20,10 @@ use Throwable;
*/
class CalendarDeletionDefaultUpdaterListener implements IEventListener {
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(IConfig $config,
- LoggerInterface $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php
index 51f31a12f8b..a58fb3524ab 100644
--- a/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php
+++ b/apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -30,12 +13,12 @@ use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend;
use OCA\DAV\CalDAV\Reminder\ReminderService;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectDeletedEvent;
-use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectRestoredEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent;
+use OCP\Calendar\Events\CalendarObjectRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
@@ -45,26 +28,12 @@ use function sprintf;
/** @template-implements IEventListener<CalendarMovedToTrashEvent|CalendarDeletedEvent|CalendarRestoredEvent|CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectMovedToTrashEvent|CalendarObjectRestoredEvent|CalendarObjectDeletedEvent> */
class CalendarObjectReminderUpdaterListener implements IEventListener {
- /** @var ReminderBackend */
- private $reminderBackend;
-
- /** @var ReminderService */
- private $reminderService;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(ReminderBackend $reminderBackend,
- ReminderService $reminderService,
- CalDavBackend $calDavBackend,
- LoggerInterface $logger) {
- $this->reminderBackend = $reminderBackend;
- $this->reminderService = $reminderService;
- $this->calDavBackend = $calDavBackend;
- $this->logger = $logger;
+ public function __construct(
+ private ReminderBackend $reminderBackend,
+ private ReminderService $reminderService,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/CalendarPublicationListener.php b/apps/dav/lib/Listener/CalendarPublicationListener.php
index 7ff419324a9..94a0a208d4e 100644
--- a/apps/dav/lib/Listener/CalendarPublicationListener.php
+++ b/apps/dav/lib/Listener/CalendarPublicationListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -34,13 +17,10 @@ use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<CalendarPublishedEvent|CalendarUnpublishedEvent> */
class CalendarPublicationListener implements IEventListener {
- private Backend $activityBackend;
- private LoggerInterface $logger;
-
- public function __construct(Backend $activityBackend,
- LoggerInterface $logger) {
- $this->activityBackend = $activityBackend;
- $this->logger = $logger;
+ public function __construct(
+ private Backend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Listener/CalendarShareUpdateListener.php b/apps/dav/lib/Listener/CalendarShareUpdateListener.php
index 53c4a9d4438..b673d5d2e42 100644
--- a/apps/dav/lib/Listener/CalendarShareUpdateListener.php
+++ b/apps/dav/lib/Listener/CalendarShareUpdateListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -33,13 +16,10 @@ use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<CalendarShareUpdatedEvent> */
class CalendarShareUpdateListener implements IEventListener {
- private Backend $activityBackend;
- private LoggerInterface $logger;
-
- public function __construct(Backend $activityBackend,
- LoggerInterface $logger) {
- $this->activityBackend = $activityBackend;
- $this->logger = $logger;
+ public function __construct(
+ private Backend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -51,7 +31,7 @@ class CalendarShareUpdateListener implements IEventListener {
return;
}
- $this->logger->debug("Creating activity for Calendar having its shares updated");
+ $this->logger->debug('Creating activity for Calendar having its shares updated');
$this->activityBackend->onCalendarUpdateShares(
$event->getCalendarData(),
@@ -59,5 +39,7 @@ class CalendarShareUpdateListener implements IEventListener {
$event->getAdded(),
$event->getRemoved()
);
+
+ // Here we should recalculate if reminders should be sent to new or old sharees
}
}
diff --git a/apps/dav/lib/Listener/CardListener.php b/apps/dav/lib/Listener/CardListener.php
index 57acde4bd8e..b9fd1a7f64b 100644
--- a/apps/dav/lib/Listener/CardListener.php
+++ b/apps/dav/lib/Listener/CardListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -38,16 +21,10 @@ use function sprintf;
/** @template-implements IEventListener<CardCreatedEvent|CardUpdatedEvent|CardDeletedEvent> */
class CardListener implements IEventListener {
- /** @var ActivityBackend */
- private $activityBackend;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(ActivityBackend $activityBackend,
- LoggerInterface $logger) {
- $this->activityBackend = $activityBackend;
- $this->logger = $logger;
+ public function __construct(
+ private ActivityBackend $activityBackend,
+ private LoggerInterface $logger,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/ClearPhotoCacheListener.php b/apps/dav/lib/Listener/ClearPhotoCacheListener.php
index 605e54aa3bc..eb599d33871 100644
--- a/apps/dav/lib/Listener/ClearPhotoCacheListener.php
+++ b/apps/dav/lib/Listener/ClearPhotoCacheListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -33,10 +16,9 @@ use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<CardUpdatedEvent|CardDeletedEvent> */
class ClearPhotoCacheListener implements IEventListener {
- private PhotoCache $photoCache;
-
- public function __construct(PhotoCache $photoCache) {
- $this->photoCache = $photoCache;
+ public function __construct(
+ private PhotoCache $photoCache,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/DavAdminSettingsListener.php b/apps/dav/lib/Listener/DavAdminSettingsListener.php
new file mode 100644
index 00000000000..69501915208
--- /dev/null
+++ b/apps/dav/lib/Listener/DavAdminSettingsListener.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\AppInfo\Application;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IAppConfig;
+use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
+use OCP\Settings\Events\DeclarativeSettingsSetValueEvent;
+
+/** @template-implements IEventListener<DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent> */
+class DavAdminSettingsListener implements IEventListener {
+
+ public function __construct(
+ private IAppConfig $config,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+
+ /** @var DeclarativeSettingsGetValueEvent|DeclarativeSettingsSetValueEvent $event */
+ if ($event->getApp() !== Application::APP_ID) {
+ return;
+ }
+
+ if ($event->getFormId() !== 'dav-admin-system-address-book') {
+ return;
+ }
+
+ if ($event instanceof DeclarativeSettingsGetValueEvent) {
+ $this->handleGetValue($event);
+ return;
+ }
+
+ if ($event instanceof DeclarativeSettingsSetValueEvent) {
+ $this->handleSetValue($event);
+ return;
+ }
+
+ }
+
+ private function handleGetValue(DeclarativeSettingsGetValueEvent $event): void {
+
+ if ($event->getFieldId() === 'system_addressbook_enabled') {
+ $event->setValue((int)$this->config->getValueBool('dav', 'system_addressbook_exposed', true));
+ }
+
+ }
+
+ private function handleSetValue(DeclarativeSettingsSetValueEvent $event): void {
+
+ if ($event->getFieldId() === 'system_addressbook_enabled') {
+ $this->config->setValueBool('dav', 'system_addressbook_exposed', (bool)$event->getValue());
+ $event->stopPropagation();
+ }
+
+ }
+
+}
diff --git a/apps/dav/lib/Listener/OutOfOfficeListener.php b/apps/dav/lib/Listener/OutOfOfficeListener.php
index 6adc42ceeba..45728aa35d3 100644
--- a/apps/dav/lib/Listener/OutOfOfficeListener.php
+++ b/apps/dav/lib/Listener/OutOfOfficeListener.php
@@ -3,24 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -55,7 +39,7 @@ class OutOfOfficeListener implements IEventListener {
private ServerFactory $serverFactory,
private IConfig $appConfig,
private TimezoneService $timezoneService,
- private LoggerInterface $logger
+ private LoggerInterface $logger,
) {
}
diff --git a/apps/dav/lib/Listener/SubscriptionListener.php b/apps/dav/lib/Listener/SubscriptionListener.php
index 645a33e690d..fc9dfcf122d 100644
--- a/apps/dav/lib/Listener/SubscriptionListener.php
+++ b/apps/dav/lib/Listener/SubscriptionListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -37,17 +20,12 @@ use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<SubscriptionCreatedEvent|SubscriptionDeletedEvent> */
class SubscriptionListener implements IEventListener {
- private IJobList $jobList;
- private RefreshWebcalService $refreshWebcalService;
- private ReminderBackend $reminderBackend;
- private LoggerInterface $logger;
-
- public function __construct(IJobList $jobList, RefreshWebcalService $refreshWebcalService, ReminderBackend $reminderBackend,
- LoggerInterface $logger) {
- $this->jobList = $jobList;
- $this->refreshWebcalService = $refreshWebcalService;
- $this->reminderBackend = $reminderBackend;
- $this->logger = $logger;
+ public function __construct(
+ private IJobList $jobList,
+ private RefreshWebcalService $refreshWebcalService,
+ private ReminderBackend $reminderBackend,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/Listener/TrustedServerRemovedListener.php b/apps/dav/lib/Listener/TrustedServerRemovedListener.php
index 7be84a5b779..9adbcfc14c2 100644
--- a/apps/dav/lib/Listener/TrustedServerRemovedListener.php
+++ b/apps/dav/lib/Listener/TrustedServerRemovedListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Carl Schwan <carl@carlschwan.eu>
- *
- * @author Carl Schwan <carl@carlschwan.eu>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -32,10 +15,9 @@ use OCP\Federation\Events\TrustedServerRemovedEvent;
/** @template-implements IEventListener<TrustedServerRemovedEvent> */
class TrustedServerRemovedListener implements IEventListener {
- private CardDavBackend $cardDavBackend;
-
- public function __construct(CardDavBackend $cardDavBackend) {
- $this->cardDavBackend = $cardDavBackend;
+ public function __construct(
+ private CardDavBackend $cardDavBackend,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Listener/UserEventsListener.php b/apps/dav/lib/Listener/UserEventsListener.php
new file mode 100644
index 00000000000..a6b09b70fa0
--- /dev/null
+++ b/apps/dav/lib/Listener/UserEventsListener.php
@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Listener;
+
+use OCA\DAV\BackgroundJob\UserStatusAutomation;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\Accounts\UserUpdatedEvent;
+use OCP\BackgroundJob\IJobList;
+use OCP\Defaults;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\Events\BeforeUserDeletedEvent;
+use OCP\User\Events\BeforeUserIdUnassignedEvent;
+use OCP\User\Events\UserChangedEvent;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserFirstTimeLoggedInEvent;
+use OCP\User\Events\UserIdAssignedEvent;
+use OCP\User\Events\UserIdUnassignedEvent;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<UserFirstTimeLoggedInEvent|UserIdAssignedEvent|BeforeUserIdUnassignedEvent|UserIdUnassignedEvent|BeforeUserDeletedEvent|UserDeletedEvent|UserCreatedEvent|UserChangedEvent|UserUpdatedEvent> */
+class UserEventsListener implements IEventListener {
+
+ /** @var IUser[] */
+ private array $usersToDelete = [];
+
+ private array $calendarsToDelete = [];
+ private array $subscriptionsToDelete = [];
+ private array $addressBooksToDelete = [];
+
+ public function __construct(
+ private IUserManager $userManager,
+ private SyncService $syncService,
+ private CalDavBackend $calDav,
+ private CardDavBackend $cardDav,
+ private Defaults $themingDefaults,
+ private ExampleContactService $exampleContactService,
+ private ExampleEventService $exampleEventService,
+ private LoggerInterface $logger,
+ private IJobList $jobList,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof UserCreatedEvent) {
+ $this->postCreateUser($event->getUser());
+ } elseif ($event instanceof UserIdAssignedEvent) {
+ $user = $this->userManager->get($event->getUserId());
+ if ($user !== null) {
+ $this->postCreateUser($user);
+ }
+ } elseif ($event instanceof BeforeUserDeletedEvent) {
+ $this->preDeleteUser($event->getUser());
+ } elseif ($event instanceof BeforeUserIdUnassignedEvent) {
+ $this->preUnassignedUserId($event->getUserId());
+ } elseif ($event instanceof UserDeletedEvent) {
+ $this->postDeleteUser($event->getUid());
+ } elseif ($event instanceof UserIdUnassignedEvent) {
+ $this->postDeleteUser($event->getUserId());
+ } elseif ($event instanceof UserChangedEvent) {
+ $this->changeUser($event->getUser(), $event->getFeature());
+ } elseif ($event instanceof UserFirstTimeLoggedInEvent) {
+ $this->firstLogin($event->getUser());
+ } elseif ($event instanceof UserUpdatedEvent) {
+ $this->updateUser($event->getUser());
+ }
+ }
+
+ public function postCreateUser(IUser $user): void {
+ $this->syncService->updateUser($user);
+ }
+
+ public function updateUser(IUser $user): void {
+ $this->syncService->updateUser($user);
+ }
+
+ public function preDeleteUser(IUser $user): void {
+ $uid = $user->getUID();
+ $userPrincipalUri = 'principals/users/' . $uid;
+ $this->usersToDelete[$uid] = $user;
+ $this->calendarsToDelete[$uid] = $this->calDav->getUsersOwnCalendars($userPrincipalUri);
+ $this->subscriptionsToDelete[$uid] = $this->calDav->getSubscriptionsForUser($userPrincipalUri);
+ $this->addressBooksToDelete[$uid] = $this->cardDav->getUsersOwnAddressBooks($userPrincipalUri);
+ }
+
+ public function preUnassignedUserId(string $uid): void {
+ $user = $this->userManager->get($uid);
+ if ($user !== null) {
+ $this->usersToDelete[$uid] = $user;
+ }
+ }
+
+ public function postDeleteUser(string $uid): void {
+ if (isset($this->usersToDelete[$uid])) {
+ $this->syncService->deleteUser($this->usersToDelete[$uid]);
+ }
+
+ foreach ($this->calendarsToDelete[$uid] as $calendar) {
+ $this->calDav->deleteCalendar(
+ $calendar['id'],
+ true // Make sure the data doesn't go into the trashbin, a new user with the same UID would later see it otherwise
+ );
+ }
+
+ foreach ($this->subscriptionsToDelete[$uid] as $subscription) {
+ $this->calDav->deleteSubscription(
+ $subscription['id'],
+ );
+ }
+ $this->calDav->deleteAllSharesByUser('principals/users/' . $uid);
+
+ foreach ($this->addressBooksToDelete[$uid] as $addressBook) {
+ $this->cardDav->deleteAddressBook($addressBook['id']);
+ }
+
+ $this->jobList->remove(UserStatusAutomation::class, ['userId' => $uid]);
+
+ unset($this->calendarsToDelete[$uid]);
+ unset($this->subscriptionsToDelete[$uid]);
+ unset($this->addressBooksToDelete[$uid]);
+ }
+
+ public function changeUser(IUser $user, string $feature): void {
+ // This case is already covered by the account manager firing up a signal
+ // later on
+ if ($feature !== 'eMailAddress' && $feature !== 'displayName') {
+ $this->syncService->updateUser($user);
+ }
+ }
+
+ public function firstLogin(IUser $user): void {
+ $principal = 'principals/users/' . $user->getUID();
+
+ $calendarId = null;
+ if ($this->calDav->getCalendarsForUserCount($principal) === 0) {
+ try {
+ $calendarId = $this->calDav->createCalendar($principal, CalDavBackend::PERSONAL_CALENDAR_URI, [
+ '{DAV:}displayname' => CalDavBackend::PERSONAL_CALENDAR_NAME,
+ '{http://apple.com/ns/ical/}calendar-color' => $this->themingDefaults->getColorPrimary(),
+ 'components' => 'VEVENT'
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+ if ($calendarId !== null) {
+ try {
+ $this->exampleEventService->createExampleEvent($calendarId);
+ } catch (\Exception $e) {
+ $this->logger->error('Failed to create example event: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'userId' => $user->getUID(),
+ 'calendarId' => $calendarId,
+ ]);
+ }
+ }
+
+ $addressBookId = null;
+ if ($this->cardDav->getAddressBooksForUserCount($principal) === 0) {
+ try {
+ $addressBookId = $this->cardDav->createAddressBook($principal, CardDavBackend::PERSONAL_ADDRESSBOOK_URI, [
+ '{DAV:}displayname' => CardDavBackend::PERSONAL_ADDRESSBOOK_NAME,
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+ if ($addressBookId) {
+ $this->exampleContactService->createDefaultContact($addressBookId);
+ }
+ }
+}
diff --git a/apps/dav/lib/Listener/UserPreferenceListener.php b/apps/dav/lib/Listener/UserPreferenceListener.php
index 885ebbc36c6..5f5fed05348 100644
--- a/apps/dav/lib/Listener/UserPreferenceListener.php
+++ b/apps/dav/lib/Listener/UserPreferenceListener.php
@@ -2,25 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Listener;
@@ -34,10 +17,9 @@ use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<BeforePreferenceSetEvent|BeforePreferenceDeletedEvent> */
class UserPreferenceListener implements IEventListener {
- protected IJobList $jobList;
-
- public function __construct(IJobList $jobList) {
- $this->jobList = $jobList;
+ public function __construct(
+ protected IJobList $jobList,
+ ) {
}
public function handle(Event $event): void {
diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php
index fc9a6d9a559..d8f906f22ee 100644
--- a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php
+++ b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -34,26 +14,11 @@ use OCP\Migration\IRepairStep;
class BuildCalendarSearchIndex implements IRepairStep {
- /** @var IDBConnection */
- private $db;
-
- /** @var IJobList */
- private $jobList;
-
- /** @var IConfig */
- private $config;
-
- /**
- * @param IDBConnection $db
- * @param IJobList $jobList
- * @param IConfig $config
- */
- public function __construct(IDBConnection $db,
- IJobList $jobList,
- IConfig $config) {
- $this->db = $db;
- $this->jobList = $jobList;
- $this->config = $config;
+ public function __construct(
+ private IDBConnection $db,
+ private IJobList $jobList,
+ private IConfig $config,
+ ) {
}
/**
@@ -76,8 +41,8 @@ class BuildCalendarSearchIndex implements IRepairStep {
$query = $this->db->getQueryBuilder();
$query->select($query->createFunction('MAX(' . $query->getColumnName('id') . ')'))
->from('calendarobjects');
- $result = $query->execute();
- $maxId = (int) $result->fetchOne();
+ $result = $query->executeQuery();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
$output->info('Add background job');
diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php
index 0b89a3d1d77..da8f31e7d3d 100644
--- a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php
+++ b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -40,16 +21,16 @@ class BuildCalendarSearchIndexBackgroundJob extends QueuedJob {
private CalDavBackend $calDavBackend,
private LoggerInterface $logger,
private IJobList $jobList,
- ITimeFactory $timeFactory
+ ITimeFactory $timeFactory,
) {
parent::__construct($timeFactory);
}
public function run($arguments) {
- $offset = (int) $arguments['offset'];
- $stopAt = (int) $arguments['stopAt'];
+ $offset = (int)$arguments['offset'];
+ $stopAt = (int)$arguments['stopAt'];
- $this->logger->info('Building calendar index (' . $offset .'/' . $stopAt . ')');
+ $this->logger->info('Building calendar index (' . $offset . '/' . $stopAt . ')');
$startTime = $this->time->getTime();
while (($this->time->getTime() - $startTime) < 15) {
diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndex.php b/apps/dav/lib/Migration/BuildSocialSearchIndex.php
index f3872acc3ab..a808034365a 100644
--- a/apps/dav/lib/Migration/BuildSocialSearchIndex.php
+++ b/apps/dav/lib/Migration/BuildSocialSearchIndex.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author call-me-matt <nextcloud@matthiasheinisch.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -31,26 +14,16 @@ use OCP\Migration\IRepairStep;
class BuildSocialSearchIndex implements IRepairStep {
- /** @var IDBConnection */
- private $db;
-
- /** @var IJobList */
- private $jobList;
-
- /** @var IConfig */
- private $config;
-
/**
* @param IDBConnection $db
* @param IJobList $jobList
* @param IConfig $config
*/
- public function __construct(IDBConnection $db,
- IJobList $jobList,
- IConfig $config) {
- $this->db = $db;
- $this->jobList = $jobList;
- $this->config = $config;
+ public function __construct(
+ private IDBConnection $db,
+ private IJobList $jobList,
+ private IConfig $config,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php
index 9a688970597..fab61d56fd6 100644
--- a/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php
+++ b/apps/dav/lib/Migration/BuildSocialSearchIndexBackgroundJob.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2020 Matthias Heinisch <nextcloud@matthiasheinisch.de>
- *
- * @author call-me-matt <nextcloud@matthiasheinisch.de>
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -30,6 +12,7 @@ use OCA\DAV\CardDAV\CardDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
+use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
@@ -48,7 +31,7 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob {
$offset = $arguments['offset'];
$stopAt = $arguments['stopAt'];
- $this->logger->info('Indexing social profile data (' . $offset .'/' . $stopAt . ')');
+ $this->logger->info('Indexing social profile data (' . $offset . '/' . $stopAt . ')');
$offset = $this->buildIndex($offset, $stopAt);
@@ -77,6 +60,7 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob {
->from('cards', 'c')
->orderBy('id', 'ASC')
->where($query->expr()->like('carddata', $query->createNamedParameter('%SOCIALPROFILE%')))
+ ->andWhere($query->expr()->gt('id', $query->createNamedParameter((int)$offset, IQueryBuilder::PARAM_INT)))
->setMaxResults(100);
$social_cards = $query->executeQuery()->fetchAll();
@@ -87,7 +71,11 @@ class BuildSocialSearchIndexBackgroundJob extends QueuedJob {
// refresh identified contacts in order to re-index
foreach ($social_cards as $contact) {
$offset = $contact['id'];
- $this->davBackend->updateCard($contact['addressbookid'], $contact['uri'], $contact['carddata']);
+ $cardData = $contact['carddata'];
+ if (is_resource($cardData) && (get_resource_type($cardData) === 'stream')) {
+ $cardData = stream_get_contents($cardData);
+ }
+ $this->davBackend->updateCard($contact['addressbookid'], $contact['uri'], $cardData);
// stop after 15sec (to be continued with next chunk)
if (($this->time->getTime() - $startTime) > 15) {
diff --git a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
index 9b96df2ae1e..24e182e46eb 100644
--- a/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
+++ b/apps/dav/lib/Migration/CalDAVRemoveEmptyValue.php
@@ -1,30 +1,11 @@
<?php
+
/**
- * @copyright 2017 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
-use Doctrine\DBAL\Platforms\OraclePlatform;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
@@ -35,18 +16,11 @@ use Sabre\VObject\InvalidDataException;
class CalDAVRemoveEmptyValue implements IRepairStep {
- /** @var IDBConnection */
- private $db;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- private LoggerInterface $logger;
-
- public function __construct(IDBConnection $db, CalDavBackend $calDavBackend, LoggerInterface $logger) {
- $this->db = $db;
- $this->calDavBackend = $calDavBackend;
- $this->logger = $logger;
+ public function __construct(
+ private IDBConnection $db,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ ) {
}
public function getName() {
@@ -94,13 +68,13 @@ class CalDAVRemoveEmptyValue implements IRepairStep {
}
protected function getInvalidObjects($pattern) {
- if ($this->db->getDatabasePlatform() instanceof OraclePlatform) {
+ if ($this->db->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
$rows = [];
$chunkSize = 500;
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('*', 'num_entries'))
->from('calendarobjects');
- $result = $query->execute();
+ $result = $query->executeQuery();
$count = $result->fetchOne();
$result->closeCursor();
@@ -112,7 +86,7 @@ class CalDAVRemoveEmptyValue implements IRepairStep {
->setMaxResults($chunkSize);
for ($chunk = 0; $chunk < $numChunks; $chunk++) {
$query->setFirstResult($chunk * $chunkSize);
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
if (mb_strpos($row['calendardata'], $pattern) !== false) {
@@ -137,7 +111,7 @@ class CalDAVRemoveEmptyValue implements IRepairStep {
IQueryBuilder::PARAM_STR
));
- $result = $query->execute();
+ $result = $query->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
diff --git a/apps/dav/lib/Migration/ChunkCleanup.php b/apps/dav/lib/Migration/ChunkCleanup.php
index 8a4ad5664a4..edd9a26109e 100644
--- a/apps/dav/lib/Migration/ChunkCleanup.php
+++ b/apps/dav/lib/Migration/ChunkCleanup.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -38,23 +21,12 @@ use OCP\Migration\IRepairStep;
class ChunkCleanup implements IRepairStep {
- /** @var IConfig */
- private $config;
- /** @var IUserManager */
- private $userManager;
- /** @var IRootFolder */
- private $rootFolder;
- /** @var IJobList */
- private $jobList;
-
- public function __construct(IConfig $config,
- IUserManager $userManager,
- IRootFolder $rootFolder,
- IJobList $jobList) {
- $this->config = $config;
- $this->userManager = $userManager;
- $this->rootFolder = $rootFolder;
- $this->jobList = $jobList;
+ public function __construct(
+ private IConfig $config,
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ private IJobList $jobList,
+ ) {
}
public function getName(): string {
@@ -65,11 +37,12 @@ class ChunkCleanup implements IRepairStep {
// If we already ran this onec there is no need to run it again
if ($this->config->getAppValue('dav', 'chunks_migrated', '0') === '1') {
$output->info('Cleanup not required');
+ return;
}
$output->startProgress();
// Loop over all seen users
- $this->userManager->callForSeenUsers(function (IUser $user) use ($output) {
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($output): void {
try {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$userRoot = $userFolder->getParent();
diff --git a/apps/dav/lib/Migration/CreateSystemAddressBookStep.php b/apps/dav/lib/Migration/CreateSystemAddressBookStep.php
new file mode 100644
index 00000000000..ec07c72e7a7
--- /dev/null
+++ b/apps/dav/lib/Migration/CreateSystemAddressBookStep.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Migration;
+
+use OCA\DAV\CardDAV\SyncService;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class CreateSystemAddressBookStep implements IRepairStep {
+
+ public function __construct(
+ private SyncService $syncService,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Create system address book';
+ }
+
+ public function run(IOutput $output): void {
+ $this->syncService->ensureLocalSystemAddressBookExists();
+ }
+}
diff --git a/apps/dav/lib/Migration/DeleteSchedulingObjects.php b/apps/dav/lib/Migration/DeleteSchedulingObjects.php
new file mode 100644
index 00000000000..3cb3c9c9b10
--- /dev/null
+++ b/apps/dav/lib/Migration/DeleteSchedulingObjects.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Migration;
+
+use OCA\DAV\BackgroundJob\DeleteOutdatedSchedulingObjects;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class DeleteSchedulingObjects implements IRepairStep {
+ public function __construct(
+ private IJobList $jobList,
+ private ITimeFactory $time,
+ private CalDavBackend $calDavBackend,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Handle outdated scheduling events';
+ }
+
+ public function run(IOutput $output): void {
+ $output->info('Cleaning up old scheduling events');
+ $time = $this->time->getTime() - (60 * 60);
+ $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000);
+ if (!$this->jobList->has(DeleteOutdatedSchedulingObjects::class, null)) {
+ $output->info('Adding background job to delete old scheduling objects');
+ $this->jobList->add(DeleteOutdatedSchedulingObjects::class, null);
+ }
+ }
+}
diff --git a/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php b/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php
index 6aa499c8b1a..6833ca2ffa6 100644
--- a/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php
+++ b/apps/dav/lib/Migration/FixBirthdayCalendarComponent.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- *
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Migration;
@@ -28,16 +13,9 @@ use OCP\Migration\IRepairStep;
class FixBirthdayCalendarComponent implements IRepairStep {
- /** @var IDBConnection */
- private $connection;
-
- /**
- * FixBirthdayCalendarComponent constructor.
- *
- * @param IDBConnection $connection
- */
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
}
/**
@@ -55,7 +33,7 @@ class FixBirthdayCalendarComponent implements IRepairStep {
$updated = $query->update('calendars')
->set('components', $query->createNamedParameter('VEVENT'))
->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
- ->execute();
+ ->executeStatement();
$output->info("$updated birthday calendars updated.");
}
diff --git a/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php b/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php
index ae930712859..cd4b8b31f4d 100644
--- a/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php
+++ b/apps/dav/lib/Migration/RefreshWebcalJobRegistrar.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -34,21 +16,16 @@ use OCP\Migration\IRepairStep;
class RefreshWebcalJobRegistrar implements IRepairStep {
- /** @var IDBConnection */
- private $connection;
-
- /** @var IJobList */
- private $jobList;
-
/**
* FixBirthdayCalendarComponent constructor.
*
* @param IDBConnection $connection
* @param IJobList $jobList
*/
- public function __construct(IDBConnection $connection, IJobList $jobList) {
- $this->connection = $connection;
- $this->jobList = $jobList;
+ public function __construct(
+ private IDBConnection $connection,
+ private IJobList $jobList,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php
index a8138d876e9..ef8e9002e9d 100644
--- a/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php
+++ b/apps/dav/lib/Migration/RegenerateBirthdayCalendars.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -31,20 +14,14 @@ use OCP\Migration\IRepairStep;
class RegenerateBirthdayCalendars implements IRepairStep {
- /** @var IJobList */
- private $jobList;
-
- /** @var IConfig */
- private $config;
-
/**
* @param IJobList $jobList
* @param IConfig $config
*/
- public function __construct(IJobList $jobList,
- IConfig $config) {
- $this->jobList = $jobList;
- $this->config = $config;
+ public function __construct(
+ private IJobList $jobList,
+ private IConfig $config,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php
index 0b5062fcf3e..7f74390f883 100644
--- a/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php
+++ b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -42,15 +22,6 @@ use OCP\Migration\IRepairStep;
*/
class RegisterBuildReminderIndexBackgroundJob implements IRepairStep {
- /** @var IDBConnection */
- private $db;
-
- /** @var IJobList */
- private $jobList;
-
- /** @var IConfig */
- private $config;
-
/** @var string */
private const CONFIG_KEY = 'buildCalendarReminderIndex';
@@ -59,12 +30,11 @@ class RegisterBuildReminderIndexBackgroundJob implements IRepairStep {
* @param IJobList $jobList
* @param IConfig $config
*/
- public function __construct(IDBConnection $db,
- IJobList $jobList,
- IConfig $config) {
- $this->db = $db;
- $this->jobList = $jobList;
- $this->config = $config;
+ public function __construct(
+ private IDBConnection $db,
+ private IJobList $jobList,
+ private IConfig $config,
+ ) {
}
/**
@@ -87,8 +57,8 @@ class RegisterBuildReminderIndexBackgroundJob implements IRepairStep {
$query = $this->db->getQueryBuilder();
$query->select($query->createFunction('MAX(' . $query->getColumnName('id') . ')'))
->from('calendarobjects');
- $result = $query->execute();
- $maxId = (int) $result->fetchOne();
+ $result = $query->executeQuery();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
$output->info('Add background job');
diff --git a/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php b/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php
new file mode 100644
index 00000000000..9d77aefafd2
--- /dev/null
+++ b/apps/dav/lib/Migration/RegisterUpdateCalendarResourcesRoomBackgroundJob.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Migration;
+
+use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RegisterUpdateCalendarResourcesRoomBackgroundJob implements IRepairStep {
+ public function __construct(
+ private readonly IJobList $jobList,
+ ) {
+ }
+
+ public function getName() {
+ return 'Register a background job to update rooms and resources';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(UpdateCalendarResourcesRoomsBackgroundJob::class);
+ }
+}
diff --git a/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php b/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php
index 36108ddadfa..f0d208f4f33 100644
--- a/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php
+++ b/apps/dav/lib/Migration/RemoveClassifiedEventActivity.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -33,11 +15,9 @@ use OCP\Migration\IRepairStep;
class RemoveClassifiedEventActivity implements IRepairStep {
- /** @var IDBConnection */
- private $connection;
-
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
}
/**
@@ -76,7 +56,7 @@ class RemoveClassifiedEventActivity implements IRepairStep {
->from('calendarobjects', 'o')
->leftJoin('o', 'calendars', 'c', $query->expr()->eq('c.id', 'o.calendarid'))
->where($query->expr()->eq('o.classification', $query->createNamedParameter(CalDavBackend::CLASSIFICATION_PRIVATE)));
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
if ($row['principaluri'] === null) {
@@ -87,7 +67,7 @@ class RemoveClassifiedEventActivity implements IRepairStep {
->setParameter('type', 'calendar')
->setParameter('calendar_id', $row['calendarid'])
->setParameter('event_uid', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '"') . '%');
- $deletedEvents += $delete->execute();
+ $deletedEvents += $delete->executeStatement();
}
$result->closeCursor();
@@ -110,7 +90,7 @@ class RemoveClassifiedEventActivity implements IRepairStep {
->from('calendarobjects', 'o')
->leftJoin('o', 'calendars', 'c', $query->expr()->eq('c.id', 'o.calendarid'))
->where($query->expr()->eq('o.classification', $query->createNamedParameter(CalDavBackend::CLASSIFICATION_CONFIDENTIAL)));
- $result = $query->execute();
+ $result = $query->executeQuery();
while ($row = $result->fetch()) {
if ($row['principaluri'] === null) {
@@ -122,7 +102,7 @@ class RemoveClassifiedEventActivity implements IRepairStep {
->setParameter('calendar_id', $row['calendarid'])
->setParameter('event_uid', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '"') . '%')
->setParameter('filtered_name', '%' . $this->connection->escapeLikeParameter('{"id":"' . $row['uid'] . '","name":"Busy"') . '%');
- $deletedEvents += $delete->execute();
+ $deletedEvents += $delete->executeStatement();
}
$result->closeCursor();
diff --git a/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php b/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php
index 38d395b2c81..e2b2b701e74 100644
--- a/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php
+++ b/apps/dav/lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Thomas Citharel <nextcloud@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -32,12 +15,6 @@ use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep {
- /** @var IDBConnection */
- private $connection;
-
- /** @var IUserManager */
- private $userManager;
-
/** @var int */
private $progress = 0;
@@ -46,9 +23,10 @@ class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep {
private const SUBSCRIPTIONS_CHUNK_SIZE = 1000;
- public function __construct(IDBConnection $connection, IUserManager $userManager) {
- $this->connection = $connection;
- $this->userManager = $userManager;
+ public function __construct(
+ private IDBConnection $connection,
+ private IUserManager $userManager,
+ ) {
}
/**
@@ -113,7 +91,7 @@ class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep {
while ($row = $result->fetch()) {
$username = $this->getPrincipal($row['principaluri']);
if (!$this->userManager->userExists($username)) {
- $this->orphanSubscriptionIds[] = (int) $row['id'];
+ $this->orphanSubscriptionIds[] = (int)$row['id'];
}
}
$result->closeCursor();
diff --git a/apps/dav/lib/Migration/RemoveObjectProperties.php b/apps/dav/lib/Migration/RemoveObjectProperties.php
index b771b70e684..f09293ae0bb 100644
--- a/apps/dav/lib/Migration/RemoveObjectProperties.php
+++ b/apps/dav/lib/Migration/RemoveObjectProperties.php
@@ -1,23 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Thomas Citharel <nextcloud@tcit.fr>.
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Migration;
@@ -31,16 +16,14 @@ class RemoveObjectProperties implements IRepairStep {
private const ME_CARD_PROPERTY = '{http://calendarserver.org/ns/}me-card';
private const CALENDAR_TRANSP_PROPERTY = '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp';
- /** @var IDBConnection */
- private $connection;
-
/**
* RemoveObjectProperties constructor.
*
* @param IDBConnection $connection
*/
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
index a36d43d29df..143dc3cd1e6 100644
--- a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
+++ b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
@@ -3,108 +3,50 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
-use OCA\DAV\CalDAV\CalDavBackend;
-use OCP\DB\QueryBuilder\IQueryBuilder;
-use OCP\IDBConnection;
+use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob;
+use OCP\BackgroundJob\IJobList;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class RemoveOrphanEventsAndContacts implements IRepairStep {
-
- /** @var IDBConnection */
- private $connection;
-
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private readonly IJobList $jobList,
+ ) {
}
- /**
- * @inheritdoc
- */
public function getName(): string {
- return 'Clean up orphan event and contact data';
+ return 'Queue jobs to clean up orphan event and contact data';
}
- /**
- * @inheritdoc
- */
- public function run(IOutput $output) {
- $orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendars', 'calendarid');
- $output->info(sprintf('%d events without a calendar have been cleaned up', $orphanItems));
- $orphanItems = $this->removeOrphanChildren('calendarobjects_props', 'calendarobjects', 'objectid');
- $output->info(sprintf('%d properties without an events have been cleaned up', $orphanItems));
- $orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendars', 'calendarid');
- $output->info(sprintf('%d changes without a calendar have been cleaned up', $orphanItems));
+ public function run(IOutput $output): void {
+ $this->queueJob('calendarobjects', 'calendars', 'calendarid', '%d events without a calendar have been cleaned up');
+ $this->queueJob('calendarobjects_props', 'calendarobjects', 'objectid', '%d properties without an events have been cleaned up');
+ $this->queueJob('calendarchanges', 'calendars', 'calendarid', '%d changes without a calendar have been cleaned up');
- $orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendarsubscriptions', 'calendarid');
- $output->info(sprintf('%d cached events without a calendar subscription have been cleaned up', $orphanItems));
- $orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendarsubscriptions', 'calendarid');
- $output->info(sprintf('%d changes without a calendar subscription have been cleaned up', $orphanItems));
+ $this->queueJob('calendarobjects', 'calendarsubscriptions', 'calendarid', '%d cached events without a calendar subscription have been cleaned up');
+ $this->queueJob('calendarchanges', 'calendarsubscriptions', 'calendarid', '%d changes without a calendar subscription have been cleaned up');
- $orphanItems = $this->removeOrphanChildren('cards', 'addressbooks', 'addressbookid');
- $output->info(sprintf('%d contacts without an addressbook have been cleaned up', $orphanItems));
- $orphanItems = $this->removeOrphanChildren('cards_properties', 'cards', 'cardid');
- $output->info(sprintf('%d properties without a contact have been cleaned up', $orphanItems));
- $orphanItems = $this->removeOrphanChildren('addressbookchanges', 'addressbooks', 'addressbookid');
- $output->info(sprintf('%d changes without an addressbook have been cleaned up', $orphanItems));
+ $this->queueJob('cards', 'addressbooks', 'addressbookid', '%d contacts without an addressbook have been cleaned up');
+ $this->queueJob('cards_properties', 'cards', 'cardid', '%d properties without a contact have been cleaned up');
+ $this->queueJob('addressbookchanges', 'addressbooks', 'addressbookid', '%d changes without an addressbook have been cleaned up');
}
- protected function removeOrphanChildren($childTable, $parentTable, $parentId): int {
- $qb = $this->connection->getQueryBuilder();
-
- $qb->select('c.id')
- ->from($childTable, 'c')
- ->leftJoin('c', $parentTable, 'p', $qb->expr()->eq('c.' . $parentId, 'p.id'))
- ->where($qb->expr()->isNull('p.id'));
-
- if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) {
- $calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR;
- $qb->andWhere($qb->expr()->eq('c.calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- }
-
- $result = $qb->execute();
-
- $orphanItems = [];
- while ($row = $result->fetch()) {
- $orphanItems[] = (int) $row['id'];
- }
- $result->closeCursor();
-
- if (!empty($orphanItems)) {
- $qb->delete($childTable)
- ->where($qb->expr()->in('id', $qb->createParameter('ids')));
-
- $orphanItemsBatch = array_chunk($orphanItems, 200);
- foreach ($orphanItemsBatch as $items) {
- $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
- $qb->execute();
- }
- }
-
- return count($orphanItems);
+ private function queueJob(
+ string $childTable,
+ string $parentTable,
+ string $parentId,
+ string $logMessage,
+ ): void {
+ $this->jobList->add(CleanupOrphanedChildrenJob::class, [
+ CleanupOrphanedChildrenJob::ARGUMENT_CHILD_TABLE => $childTable,
+ CleanupOrphanedChildrenJob::ARGUMENT_PARENT_TABLE => $parentTable,
+ CleanupOrphanedChildrenJob::ARGUMENT_PARENT_ID => $parentId,
+ CleanupOrphanedChildrenJob::ARGUMENT_LOG_MESSAGE => $logMessage,
+ ]);
}
}
diff --git a/apps/dav/lib/Migration/Version1004Date20170825134824.php b/apps/dav/lib/Migration/Version1004Date20170825134824.php
index a7cbaa78ef2..4bf9637b697 100644
--- a/apps/dav/lib/Migration/Version1004Date20170825134824.php
+++ b/apps/dav/lib/Migration/Version1004Date20170825134824.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -383,6 +364,7 @@ class Version1004Date20170825134824 extends SimpleMigrationStep {
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['principaluri'], 'schedulobj_principuri_index');
+ $table->addIndex(['lastmodified'], 'schedulobj_lastmodified_idx');
}
if (!$schema->hasTable('cards_properties')) {
@@ -490,6 +472,9 @@ class Version1004Date20170825134824 extends SimpleMigrationStep {
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['principaluri', 'resourceid', 'type', 'publicuri'], 'dav_shares_index');
+ // modified on 2024-6-21 to add performance improving indices on new instances
+ $table->addIndex(['resourceid', 'type'], 'dav_shares_resourceid_type');
+ $table->addIndex(['resourceid', 'access'], 'dav_shares_resourceid_access');
}
return $schema;
}
diff --git a/apps/dav/lib/Migration/Version1004Date20170919104507.php b/apps/dav/lib/Migration/Version1004Date20170919104507.php
index 54548f30aca..678d92d2b83 100644
--- a/apps/dav/lib/Migration/Version1004Date20170919104507.php
+++ b/apps/dav/lib/Migration/Version1004Date20170919104507.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1004Date20170924124212.php b/apps/dav/lib/Migration/Version1004Date20170924124212.php
index 4cc795e1aec..4d221e91132 100644
--- a/apps/dav/lib/Migration/Version1004Date20170924124212.php
+++ b/apps/dav/lib/Migration/Version1004Date20170924124212.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -46,7 +29,10 @@ class Version1004Date20170924124212 extends SimpleMigrationStep {
$table->addIndex(['addressbookid', 'uri'], 'cards_abiduri');
$table = $schema->getTable('cards_properties');
- $table->addIndex(['addressbookid'], 'cards_prop_abid');
+ // Removed later on
+ // $table->addIndex(['addressbookid'], 'cards_prop_abid');
+ // Added later on
+ $table->addIndex(['addressbookid', 'name', 'value'], 'cards_prop_abid_name_value', );
return $schema;
}
diff --git a/apps/dav/lib/Migration/Version1004Date20170926103422.php b/apps/dav/lib/Migration/Version1004Date20170926103422.php
index 6d88effc240..ec56e035006 100644
--- a/apps/dav/lib/Migration/Version1004Date20170926103422.php
+++ b/apps/dav/lib/Migration/Version1004Date20170926103422.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -32,7 +15,7 @@ class Version1004Date20170926103422 extends BigIntMigration {
/**
* @return array Returns an array with the following structure
- * ['table1' => ['column1', 'column2'], ...]
+ * ['table1' => ['column1', 'column2'], ...]
* @since 13.0.0
*/
protected function getColumnsByTable() {
diff --git a/apps/dav/lib/Migration/Version1005Date20180413093149.php b/apps/dav/lib/Migration/Version1005Date20180413093149.php
index 6609b2396dd..db071c65d98 100644
--- a/apps/dav/lib/Migration/Version1005Date20180413093149.php
+++ b/apps/dav/lib/Migration/Version1005Date20180413093149.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1005Date20180530124431.php b/apps/dav/lib/Migration/Version1005Date20180530124431.php
index 9b0868f9374..b5f9ff26962 100644
--- a/apps/dav/lib/Migration/Version1005Date20180530124431.php
+++ b/apps/dav/lib/Migration/Version1005Date20180530124431.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1006Date20180619154313.php b/apps/dav/lib/Migration/Version1006Date20180619154313.php
index 36e35f615ac..231861a68c4 100644
--- a/apps/dav/lib/Migration/Version1006Date20180619154313.php
+++ b/apps/dav/lib/Migration/Version1006Date20180619154313.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1006Date20180628111625.php b/apps/dav/lib/Migration/Version1006Date20180628111625.php
index 8c3a8e4938c..f4be26e6ad0 100644
--- a/apps/dav/lib/Migration/Version1006Date20180628111625.php
+++ b/apps/dav/lib/Migration/Version1006Date20180628111625.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -70,6 +49,7 @@ class Version1006Date20180628111625 extends SimpleMigrationStep {
$calendarObjectsTable->dropIndex('calobjects_index');
}
$calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uri'], 'calobjects_index');
+ $calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uid'], 'calobjects_by_uid_index');
}
if ($schema->hasTable('calendarobjects_props')) {
diff --git a/apps/dav/lib/Migration/Version1008Date20181030113700.php b/apps/dav/lib/Migration/Version1008Date20181030113700.php
index e9751159913..d2354a185df 100644
--- a/apps/dav/lib/Migration/Version1008Date20181030113700.php
+++ b/apps/dav/lib/Migration/Version1008Date20181030113700.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv@protonmail.com)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1008Date20181105104826.php b/apps/dav/lib/Migration/Version1008Date20181105104826.php
index 7b0c9861a98..82612307cbb 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105104826.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105104826.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -37,16 +17,14 @@ use OCP\Migration\SimpleMigrationStep;
class Version1008Date20181105104826 extends SimpleMigrationStep {
- /** @var IDBConnection */
- private $connection;
-
/**
* Version1008Date20181105104826 constructor.
*
* @param IDBConnection $connection
*/
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/Version1008Date20181105104833.php b/apps/dav/lib/Migration/Version1008Date20181105104833.php
index 4b4e9cc535e..3d4094d8072 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105104833.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105104833.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1008Date20181105110300.php b/apps/dav/lib/Migration/Version1008Date20181105110300.php
index 9a69d106412..72e8dee1bf8 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105110300.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105110300.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -37,16 +17,14 @@ use OCP\Migration\SimpleMigrationStep;
class Version1008Date20181105110300 extends SimpleMigrationStep {
- /** @var IDBConnection */
- private $connection;
-
/**
* Version1008Date20181105110300 constructor.
*
* @param IDBConnection $connection
*/
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
}
/**
diff --git a/apps/dav/lib/Migration/Version1008Date20181105112049.php b/apps/dav/lib/Migration/Version1008Date20181105112049.php
index 8dd7d695e68..eb18eacb0b1 100644
--- a/apps/dav/lib/Migration/Version1008Date20181105112049.php
+++ b/apps/dav/lib/Migration/Version1008Date20181105112049.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1008Date20181114084440.php b/apps/dav/lib/Migration/Version1008Date20181114084440.php
index 91f4211e3fa..3718f16f54a 100644
--- a/apps/dav/lib/Migration/Version1008Date20181114084440.php
+++ b/apps/dav/lib/Migration/Version1008Date20181114084440.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1011Date20190725113607.php b/apps/dav/lib/Migration/Version1011Date20190725113607.php
index a6673378259..4524dca9c83 100644
--- a/apps/dav/lib/Migration/Version1011Date20190725113607.php
+++ b/apps/dav/lib/Migration/Version1011Date20190725113607.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1011Date20190806104428.php b/apps/dav/lib/Migration/Version1011Date20190806104428.php
index 7c41bce87f5..183dcd4bf1e 100644
--- a/apps/dav/lib/Migration/Version1011Date20190806104428.php
+++ b/apps/dav/lib/Migration/Version1011Date20190806104428.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1012Date20190808122342.php b/apps/dav/lib/Migration/Version1012Date20190808122342.php
index e72b552cac1..717bcbc1119 100644
--- a/apps/dav/lib/Migration/Version1012Date20190808122342.php
+++ b/apps/dav/lib/Migration/Version1012Date20190808122342.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1016Date20201109085907.php b/apps/dav/lib/Migration/Version1016Date20201109085907.php
index 6828c71301a..f0f4b8b77c1 100644
--- a/apps/dav/lib/Migration/Version1016Date20201109085907.php
+++ b/apps/dav/lib/Migration/Version1016Date20201109085907.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1017Date20210216083742.php b/apps/dav/lib/Migration/Version1017Date20210216083742.php
index c5347d773d1..43bece200b8 100644
--- a/apps/dav/lib/Migration/Version1017Date20210216083742.php
+++ b/apps/dav/lib/Migration/Version1017Date20210216083742.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1018Date20210312100735.php b/apps/dav/lib/Migration/Version1018Date20210312100735.php
index b0ea5146caa..8199d3e9cd2 100644
--- a/apps/dav/lib/Migration/Version1018Date20210312100735.php
+++ b/apps/dav/lib/Migration/Version1018Date20210312100735.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1024Date20211221144219.php b/apps/dav/lib/Migration/Version1024Date20211221144219.php
index b93f8ac801e..656a50809cd 100644
--- a/apps/dav/lib/Migration/Version1024Date20211221144219.php
+++ b/apps/dav/lib/Migration/Version1024Date20211221144219.php
@@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
-
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
namespace OCA\DAV\Migration;
use Closure;
diff --git a/apps/dav/lib/Migration/Version1025Date20240308063933.php b/apps/dav/lib/Migration/Version1025Date20240308063933.php
index a75fc85eccc..d84acf8fea9 100644
--- a/apps/dav/lib/Migration/Version1025Date20240308063933.php
+++ b/apps/dav/lib/Migration/Version1025Date20240308063933.php
@@ -3,30 +3,14 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
use Closure;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
@@ -36,10 +20,10 @@ use OCP\Migration\SimpleMigrationStep;
class Version1025Date20240308063933 extends SimpleMigrationStep {
- private IDBConnection $db;
-
- public function __construct(IDBConnection $db) {
- $this->db = $db;
+ public function __construct(
+ private IAppConfig $appConfig,
+ private IDBConnection $db,
+ ) {
}
/**
@@ -67,7 +51,22 @@ class Version1025Date20240308063933 extends SimpleMigrationStep {
}
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {
+ // The threshold is higher than the default of \OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob
+ // but small enough to fit into a cluster transaction size.
+ // For a 50k users instance that would still keep 10 changes on average.
+ $limit = max(1, (int)$this->appConfig->getAppValue('totalNumberOfSyncTokensToKeep', '500000'));
+
foreach (['addressbookchanges', 'calendarchanges'] as $tableName) {
+ $thresholdSelect = $this->db->getQueryBuilder();
+ $thresholdSelect->select('id')
+ ->from($tableName)
+ ->orderBy('id', 'desc')
+ ->setFirstResult($limit)
+ ->setMaxResults(1);
+ $oldestIdResult = $thresholdSelect->executeQuery();
+ $oldestId = $oldestIdResult->fetchColumn();
+ $oldestIdResult->closeCursor();
+
$qb = $this->db->getQueryBuilder();
$update = $qb->update($tableName)
@@ -76,7 +75,15 @@ class Version1025Date20240308063933 extends SimpleMigrationStep {
$qb->expr()->eq('created_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)),
);
+ // If there is a lot of data we only set timestamp for the most recent rows
+ // because the rest will be deleted by \OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob
+ // anyway.
+ if ($oldestId !== false) {
+ $update->andWhere($qb->expr()->gt('id', $qb->createNamedParameter($oldestId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ }
+
$updated = $update->executeStatement();
+
$output->debug('Added a default creation timestamp to ' . $updated . ' rows in ' . $tableName);
}
}
diff --git a/apps/dav/lib/Migration/Version1027Date20230504122946.php b/apps/dav/lib/Migration/Version1027Date20230504122946.php
index 28361011436..89c192a8419 100644
--- a/apps/dav/lib/Migration/Version1027Date20230504122946.php
+++ b/apps/dav/lib/Migration/Version1027Date20230504122946.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -37,10 +20,12 @@ use Psr\Log\LoggerInterface;
use Throwable;
class Version1027Date20230504122946 extends SimpleMigrationStep {
- public function __construct(private SyncService $syncService,
+ public function __construct(
+ private SyncService $syncService,
private LoggerInterface $logger,
private IUserManager $userManager,
- private IConfig $config) {
+ private IConfig $config,
+ ) {
}
/**
* @param IOutput $output
@@ -48,7 +33,7 @@ class Version1027Date20230504122946 extends SimpleMigrationStep {
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
- if($this->userManager->countSeenUsers() > 100 || array_sum($this->userManager->countUsers()) > 100) {
+ if ($this->userManager->countSeenUsers() > 100 || $this->userManager->countUsersTotal(100) >= 100) {
$this->config->setAppValue('dav', 'needs_system_address_book_sync', 'yes');
$output->info('Could not sync system address books during update - too many user records have been found. Please call occ dav:sync-system-addressbook manually.');
return;
diff --git a/apps/dav/lib/Migration/Version1029Date20221114151721.php b/apps/dav/lib/Migration/Version1029Date20221114151721.php
index 112b240c8ba..dba5e0b1a48 100644
--- a/apps/dav/lib/Migration/Version1029Date20221114151721.php
+++ b/apps/dav/lib/Migration/Version1029Date20221114151721.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Murena SAS <akhil.potukuchi.ext@murena.com>
- *
- * @author Murena SAS <akhil.potukuchi.ext@murena.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
@@ -45,7 +28,7 @@ class Version1029Date20221114151721 extends SimpleMigrationStep {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$calendarObjectsTable = $schema->getTable('calendarobjects');
- if(!$calendarObjectsTable->hasIndex('calobj_clssfction_index')) {
+ if (!$calendarObjectsTable->hasIndex('calobj_clssfction_index')) {
$calendarObjectsTable->addIndex(['classification'], 'calobj_clssfction_index');
return $schema;
}
diff --git a/apps/dav/lib/Migration/Version1029Date20231004091403.php b/apps/dav/lib/Migration/Version1029Date20231004091403.php
index 6eb7be8a5cd..1dcbf9c5dfc 100644
--- a/apps/dav/lib/Migration/Version1029Date20231004091403.php
+++ b/apps/dav/lib/Migration/Version1029Date20231004091403.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1030Date20240205103243.php b/apps/dav/lib/Migration/Version1030Date20240205103243.php
index eb471ccb0a6..72cbcafc444 100644
--- a/apps/dav/lib/Migration/Version1030Date20240205103243.php
+++ b/apps/dav/lib/Migration/Version1030Date20240205103243.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2024 Johannes Merkel <mail@johannesgge.de>
- *
- * @author Johannes Merkel <mail@johannesgge.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Migration;
diff --git a/apps/dav/lib/Migration/Version1031Date20240610134258.php b/apps/dav/lib/Migration/Version1031Date20240610134258.php
new file mode 100644
index 00000000000..c1242ceb7db
--- /dev/null
+++ b/apps/dav/lib/Migration/Version1031Date20240610134258.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\Attributes\AddColumn;
+use OCP\Migration\Attributes\ColumnType;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+#[AddColumn(table: 'dav_absence', name: 'replacement_user_id', type: ColumnType::STRING)]
+#[AddColumn(table: 'dav_absence', name: 'replacement_user_display_name', type: ColumnType::STRING)]
+class Version1031Date20240610134258 extends SimpleMigrationStep {
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $tableDavAbsence = $schema->getTable('dav_absence');
+
+ if (!$tableDavAbsence->hasColumn('replacement_user_id')) {
+ $tableDavAbsence->addColumn('replacement_user_id', Types::STRING, [
+ 'notnull' => false,
+ 'default' => '',
+ 'length' => 64,
+ ]);
+ }
+
+ if (!$tableDavAbsence->hasColumn('replacement_user_display_name')) {
+ $tableDavAbsence->addColumn('replacement_user_display_name', Types::STRING, [
+ 'notnull' => false,
+ 'default' => '',
+ 'length' => 64,
+ ]);
+ }
+
+ return $schema;
+ }
+
+}
diff --git a/apps/dav/lib/Model/ExampleEvent.php b/apps/dav/lib/Model/ExampleEvent.php
new file mode 100644
index 00000000000..d2a5b8ad2d1
--- /dev/null
+++ b/apps/dav/lib/Model/ExampleEvent.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Model;
+
+use Sabre\VObject\Component\VCalendar;
+
+/**
+ * Simple DTO to store a parsed example event and its UID.
+ */
+final class ExampleEvent {
+ public function __construct(
+ private readonly VCalendar $vCalendar,
+ private readonly string $uid,
+ ) {
+ }
+
+ public function getUid(): string {
+ return $this->uid;
+ }
+
+ public function getIcs(): string {
+ return $this->vCalendar->serialize();
+ }
+}
diff --git a/apps/dav/lib/Paginate/LimitedCopyIterator.php b/apps/dav/lib/Paginate/LimitedCopyIterator.php
new file mode 100644
index 00000000000..7f19885bc7d
--- /dev/null
+++ b/apps/dav/lib/Paginate/LimitedCopyIterator.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Paginate;
+
+/**
+ * Save a copy of the first X items into a separate iterator
+ *
+ * This allows us to pass the iterator to the cache while keeping a copy
+ * of the required items.
+ *
+ * @extends \AppendIterator<int, int, \Iterator<int, int>>
+ */
+class LimitedCopyIterator extends \AppendIterator {
+ private array $skipped = [];
+ private array $copy = [];
+
+ public function __construct(\Traversable $iterator, int $count, int $offset = 0) {
+ parent::__construct();
+
+ if (!$iterator instanceof \Iterator) {
+ $iterator = new \IteratorIterator($iterator);
+ }
+ $iterator = new \NoRewindIterator($iterator);
+
+ $i = 0;
+ while ($iterator->valid() && ++$i <= $offset) {
+ $this->skipped[] = $iterator->current();
+ $iterator->next();
+ }
+
+ while ($iterator->valid() && count($this->copy) < $count) {
+ $this->copy[] = $iterator->current();
+ $iterator->next();
+ }
+
+ $this->append(new \ArrayIterator($this->skipped));
+ $this->append($this->getRequestedItems());
+ $this->append($iterator);
+ }
+
+ public function getRequestedItems(): \Iterator {
+ return new \ArrayIterator($this->copy);
+ }
+}
diff --git a/apps/dav/lib/Paginate/PaginateCache.php b/apps/dav/lib/Paginate/PaginateCache.php
new file mode 100644
index 00000000000..58219b03621
--- /dev/null
+++ b/apps/dav/lib/Paginate/PaginateCache.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Paginate;
+
+use Generator;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IDBConnection;
+use OCP\Security\ISecureRandom;
+
+class PaginateCache {
+ public const TTL = 60 * 60;
+ private const CACHE_COUNT_SUFFIX = 'count';
+
+ private ICache $cache;
+
+ public function __construct(
+ private IDBConnection $database,
+ private ISecureRandom $random,
+ ICacheFactory $cacheFactory,
+ ) {
+ $this->cache = $cacheFactory->createDistributed('pagination_');
+ }
+
+ /**
+ * @param string $uri
+ * @param \Iterator $items
+ * @return array{'token': string, 'count': int}
+ */
+ public function store(string $uri, \Iterator $items): array {
+ $token = $this->random->generate(32);
+ $cacheKey = $this->buildCacheKey($uri, $token);
+
+ $count = 0;
+ foreach ($items as $item) {
+ // Add small margin to avoid fetching valid count and then expired entries
+ $this->cache->set($cacheKey . $count, $item, self::TTL + 60);
+ ++$count;
+ }
+ $this->cache->set($cacheKey . self::CACHE_COUNT_SUFFIX, $count, self::TTL);
+
+ return ['token' => $token, 'count' => $count];
+ }
+
+ /**
+ * @return Generator<mixed>
+ */
+ public function get(string $uri, string $token, int $offset, int $count): Generator {
+ $cacheKey = $this->buildCacheKey($uri, $token);
+ $nbItems = $this->cache->get($cacheKey . self::CACHE_COUNT_SUFFIX);
+ if (!$nbItems || $offset > $nbItems) {
+ return [];
+ }
+
+ $lastItem = min($nbItems, $offset + $count);
+ for ($i = $offset; $i < $lastItem; ++$i) {
+ yield $this->cache->get($cacheKey . $i);
+ }
+ }
+
+ public function exists(string $uri, string $token): bool {
+ return $this->cache->get($this->buildCacheKey($uri, $token) . self::CACHE_COUNT_SUFFIX) > 0;
+ }
+
+ private function buildCacheKey(string $uri, string $token): string {
+ return $token . '_' . crc32($uri) . '_';
+ }
+}
diff --git a/apps/dav/lib/Paginate/PaginatePlugin.php b/apps/dav/lib/Paginate/PaginatePlugin.php
new file mode 100644
index 00000000000..c5da18f5c47
--- /dev/null
+++ b/apps/dav/lib/Paginate/PaginatePlugin.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\DAV\Paginate;
+
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class PaginatePlugin extends ServerPlugin {
+ public const PAGINATE_HEADER = 'X-NC-Paginate';
+ public const PAGINATE_TOTAL_HEADER = 'X-NC-Paginate-Total';
+ public const PAGINATE_TOKEN_HEADER = 'X-NC-Paginate-Token';
+ public const PAGINATE_OFFSET_HEADER = 'X-NC-Paginate-Offset';
+ public const PAGINATE_COUNT_HEADER = 'X-NC-Paginate-Count';
+
+ /** @var Server */
+ private $server;
+
+ public function __construct(
+ private PaginateCache $cache,
+ private int $pageSize = 100,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $this->server = $server;
+ $server->on('beforeMultiStatus', [$this, 'onMultiStatus']);
+ $server->on('method:SEARCH', [$this, 'onMethod'], 1);
+ $server->on('method:PROPFIND', [$this, 'onMethod'], 1);
+ $server->on('method:REPORT', [$this, 'onMethod'], 1);
+ }
+
+ public function getFeatures(): array {
+ return ['nc-paginate'];
+ }
+
+ public function onMultiStatus(&$fileProperties): void {
+ $request = $this->server->httpRequest;
+ if (is_array($fileProperties)) {
+ $fileProperties = new \ArrayIterator($fileProperties);
+ }
+ $url = $request->getUrl();
+ if (
+ $request->hasHeader(self::PAGINATE_HEADER)
+ && (!$request->hasHeader(self::PAGINATE_TOKEN_HEADER) || !$this->cache->exists($url, $request->getHeader(self::PAGINATE_TOKEN_HEADER)))
+ ) {
+ $pageSize = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize;
+ $offset = (int)$request->getHeader(self::PAGINATE_OFFSET_HEADER);
+ $copyIterator = new LimitedCopyIterator($fileProperties, $pageSize, $offset);
+ ['token' => $token, 'count' => $count] = $this->cache->store($url, $copyIterator);
+
+ $fileProperties = $copyIterator->getRequestedItems();
+ $this->server->httpResponse->addHeader(self::PAGINATE_HEADER, 'true');
+ $this->server->httpResponse->addHeader(self::PAGINATE_TOKEN_HEADER, $token);
+ $this->server->httpResponse->addHeader(self::PAGINATE_TOTAL_HEADER, (string)$count);
+ $request->setHeader(self::PAGINATE_TOKEN_HEADER, $token);
+ }
+ }
+
+ public function onMethod(RequestInterface $request, ResponseInterface $response) {
+ $url = $this->server->httpRequest->getUrl();
+ if (
+ $request->hasHeader(self::PAGINATE_TOKEN_HEADER)
+ && $request->hasHeader(self::PAGINATE_OFFSET_HEADER)
+ && $this->cache->exists($url, $request->getHeader(self::PAGINATE_TOKEN_HEADER))
+ ) {
+ $token = $request->getHeader(self::PAGINATE_TOKEN_HEADER);
+ $offset = (int)$request->getHeader(self::PAGINATE_OFFSET_HEADER);
+ $count = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize;
+
+ $items = $this->cache->get($url, $token, $offset, $count);
+
+ $response->setStatus(207);
+ $response->addHeader(self::PAGINATE_HEADER, 'true');
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Vary', 'Brief,Prefer');
+
+ $prefer = $this->server->getHTTPPrefer();
+ $minimal = $prefer['return'] === 'minimal';
+
+ $data = $this->server->generateMultiStatus($items, $minimal);
+ $response->setBody($data);
+
+ return false;
+ }
+ }
+}
diff --git a/apps/dav/lib/Profiler/ProfilerPlugin.php b/apps/dav/lib/Profiler/ProfilerPlugin.php
index cc65c2b75e1..455760fc2bf 100644
--- a/apps/dav/lib/Profiler/ProfilerPlugin.php
+++ b/apps/dav/lib/Profiler/ProfilerPlugin.php
@@ -2,25 +2,8 @@
declare(strict_types = 1);
/**
- * @copyright 2021 Carl Schwan <carl@carlschwan.eu>
- *
- * @author Carl Schwan <carl@carlschwan.eu>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Profiler;
@@ -31,10 +14,9 @@ use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class ProfilerPlugin extends \Sabre\DAV\ServerPlugin {
- private IRequest $request;
-
- public function __construct(IRequest $request) {
- $this->request = $request;
+ public function __construct(
+ private IRequest $request,
+ ) {
}
/** @return void */
diff --git a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php
index 614ddabb7ef..bb098a0f107 100644
--- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php
+++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningNode.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Provisioning\Apple;
@@ -32,13 +15,12 @@ use Sabre\DAV\PropPatch;
class AppleProvisioningNode implements INode, IProperties {
public const FILENAME = 'apple-provisioning.mobileconfig';
- protected $timeFactory;
-
/**
* @param ITimeFactory $timeFactory
*/
- public function __construct(ITimeFactory $timeFactory) {
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ protected ITimeFactory $timeFactory,
+ ) {
}
/**
@@ -76,7 +58,7 @@ class AppleProvisioningNode implements INode, IProperties {
return [
'{DAV:}getcontentlength' => 42,
- '{DAV:}getlastmodified' => $datetime->format(\DateTimeInterface::RFC2822),
+ '{DAV:}getlastmodified' => $datetime->format(\DateTimeInterface::RFC7231),
];
}
diff --git a/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php b/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php
index 98be4e41917..258138caa42 100644
--- a/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php
+++ b/apps/dav/lib/Provisioning/Apple/AppleProvisioningPlugin.php
@@ -1,31 +1,12 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Nils Wittenbrink <nilswittenbrink@web.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Provisioning\Apple;
+use OCP\AppFramework\Http;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
@@ -42,52 +23,22 @@ class AppleProvisioningPlugin extends ServerPlugin {
protected $server;
/**
- * @var IURLGenerator
- */
- protected $urlGenerator;
-
- /**
- * @var IUserSession
- */
- protected $userSession;
-
- /**
* @var \OC_Defaults
*/
protected $themingDefaults;
/**
- * @var IRequest
- */
- protected $request;
-
- /**
- * @var IL10N
- */
- protected $l10n;
-
- /**
- * @var \Closure
- */
- protected $uuidClosure;
-
- /**
* AppleProvisioningPlugin constructor.
*/
public function __construct(
- IUserSession $userSession,
- IURLGenerator $urlGenerator,
+ protected IUserSession $userSession,
+ protected IURLGenerator $urlGenerator,
\OC_Defaults $themingDefaults,
- IRequest $request,
- IL10N $l10n,
- \Closure $uuidClosure
+ protected IRequest $request,
+ protected IL10N $l10n,
+ protected \Closure $uuidClosure,
) {
- $this->userSession = $userSession;
- $this->urlGenerator = $urlGenerator;
$this->themingDefaults = $themingDefaults;
- $this->request = $request;
- $this->l10n = $l10n;
- $this->uuidClosure = $uuidClosure;
}
/**
@@ -117,7 +68,7 @@ class AppleProvisioningPlugin extends ServerPlugin {
$useSSL = ($serverProtocol === 'https');
if (!$useSSL) {
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setHeader('Content-Type', 'text/plain; charset=utf-8');
$response->setBody($this->l10n->t('Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.', [$this->themingDefaults->getName()]));
@@ -126,11 +77,7 @@ class AppleProvisioningPlugin extends ServerPlugin {
$absoluteURL = $this->urlGenerator->getBaseUrl();
$parsedUrl = parse_url($absoluteURL);
- if (isset($parsedUrl['port'])) {
- $serverPort = $parsedUrl['port'];
- } else {
- $serverPort = 443;
- }
+ $serverPort = $parsedUrl['port'] ?? 443;
$server_url = $parsedUrl['host'];
$description = $this->themingDefaults->getName();
@@ -154,7 +101,7 @@ class AppleProvisioningPlugin extends ServerPlugin {
$filename = $userId . '-' . AppleProvisioningNode::FILENAME;
$xmlSkeleton = $this->getTemplate();
- $body = vsprintf($xmlSkeleton, array_map(function ($v) {
+ $body = vsprintf($xmlSkeleton, array_map(function (string $v) {
return \htmlspecialchars($v, ENT_XML1, 'UTF-8');
}, [
$description,
@@ -179,7 +126,7 @@ class AppleProvisioningPlugin extends ServerPlugin {
]
));
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
$response->setHeader('Content-Type', 'application/xml; charset=utf-8');
$response->setBody($body);
diff --git a/apps/dav/lib/ResponseDefinitions.php b/apps/dav/lib/ResponseDefinitions.php
index e6de3d5a65c..3deafad6704 100644
--- a/apps/dav/lib/ResponseDefinitions.php
+++ b/apps/dav/lib/ResponseDefinitions.php
@@ -3,33 +3,20 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV;
+use OCA\DAV\CalDAV\UpcomingEvent;
+
/**
* @psalm-type DAVOutOfOfficeDataCommon = array{
* userId: string,
* message: string,
+ * replacementUserId: ?string,
+ * replacementUserDisplayName: ?string,
* }
*
* @psalm-type DAVOutOfOfficeData = DAVOutOfOfficeDataCommon&array{
@@ -46,6 +33,15 @@ namespace OCA\DAV;
* endDate: int,
* shortMessage: string,
* }
+ *
+ * @see UpcomingEvent::jsonSerialize
+ * @psalm-type DAVUpcomingEvent = array{
+ * uri: string,
+ * calendarUri: string,
+ * start: ?int,
+ * summary: ?string,
+ * location: ?string,
+ * }
*/
class ResponseDefinitions {
}
diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php
index d1f3dbc91bd..870aa0d4540 100644
--- a/apps/dav/lib/RootCollection.php
+++ b/apps/dav/lib/RootCollection.php
@@ -1,30 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV;
@@ -44,41 +23,53 @@ use OCA\DAV\Connector\Sabre\Principal;
use OCA\DAV\DAV\GroupPrincipalBackend;
use OCA\DAV\DAV\SystemPrincipalBackend;
use OCA\DAV\Provisioning\Apple\AppleProvisioningNode;
+use OCA\DAV\SystemTag\SystemTagsByIdCollection;
+use OCA\DAV\SystemTag\SystemTagsInUseCollection;
+use OCA\DAV\SystemTag\SystemTagsRelationsCollection;
use OCA\DAV\Upload\CleanupService;
use OCP\Accounts\IAccountManager;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use Psr\Log\LoggerInterface;
use Sabre\DAV\SimpleCollection;
class RootCollection extends SimpleCollection {
public function __construct() {
$l10n = \OC::$server->getL10N('dav');
- $random = \OC::$server->getSecureRandom();
- $logger = \OC::$server->get(LoggerInterface::class);
- $userManager = \OC::$server->getUserManager();
- $userSession = \OC::$server->getUserSession();
- $groupManager = \OC::$server->getGroupManager();
- $shareManager = \OC::$server->getShareManager();
- $db = \OC::$server->getDatabaseConnection();
- $dispatcher = \OC::$server->get(IEventDispatcher::class);
- $config = \OC::$server->get(IConfig::class);
- $proxyMapper = \OC::$server->query(ProxyMapper::class);
- $rootFolder = \OCP\Server::get(IRootFolder::class);
+ $random = Server::get(ISecureRandom::class);
+ $logger = Server::get(LoggerInterface::class);
+ $userManager = Server::get(IUserManager::class);
+ $userSession = Server::get(IUserSession::class);
+ $groupManager = Server::get(IGroupManager::class);
+ $shareManager = Server::get(\OCP\Share\IManager::class);
+ $db = Server::get(IDBConnection::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
+ $config = Server::get(IConfig::class);
+ $proxyMapper = Server::get(ProxyMapper::class);
+ $rootFolder = Server::get(IRootFolder::class);
$userPrincipalBackend = new Principal(
$userManager,
$groupManager,
- \OC::$server->get(IAccountManager::class),
+ Server::get(IAccountManager::class),
$shareManager,
- \OC::$server->getUserSession(),
- \OC::$server->getAppManager(),
+ Server::get(IUserSession::class),
+ Server::get(IAppManager::class),
$proxyMapper,
- \OC::$server->get(KnownUserService::class),
- \OC::$server->getConfig(),
+ Server::get(KnownUserService::class),
+ Server::get(IConfig::class),
\OC::$server->getL10NFactory()
);
@@ -96,10 +87,8 @@ class RootCollection extends SimpleCollection {
$systemPrincipals = new Collection(new SystemPrincipalBackend(), 'principals/system');
$systemPrincipals->disableListing = $disableListing;
$calendarResourcePrincipals = new Collection($calendarResourcePrincipalBackend, 'principals/calendar-resources');
- $calendarResourcePrincipals->disableListing = $disableListing;
$calendarRoomPrincipals = new Collection($calendarRoomPrincipalBackend, 'principals/calendar-rooms');
- $calendarRoomPrincipals->disableListing = $disableListing;
- $calendarSharingBackend = \OC::$server->get(Backend::class);
+ $calendarSharingBackend = Server::get(Backend::class);
$filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users');
$filesCollection->disableListing = $disableListing;
@@ -124,37 +113,35 @@ class RootCollection extends SimpleCollection {
$publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config, $logger);
- $systemTagCollection = new SystemTag\SystemTagsByIdCollection(
- \OC::$server->getSystemTagManager(),
- \OC::$server->getUserSession(),
- $groupManager
- );
- $systemTagRelationsCollection = new SystemTag\SystemTagsRelationsCollection(
- \OC::$server->getSystemTagManager(),
- \OC::$server->getSystemTagObjectMapper(),
- \OC::$server->getUserSession(),
+ $systemTagCollection = Server::get(SystemTagsByIdCollection::class);
+ $systemTagRelationsCollection = new SystemTagsRelationsCollection(
+ Server::get(ISystemTagManager::class),
+ Server::get(ISystemTagObjectMapper::class),
+ Server::get(IUserSession::class),
$groupManager,
$dispatcher,
$rootFolder,
);
- $systemTagInUseCollection = \OCP\Server::get(SystemTag\SystemTagsInUseCollection::class);
+ $systemTagInUseCollection = Server::get(SystemTagsInUseCollection::class);
$commentsCollection = new Comments\RootCollection(
- \OC::$server->getCommentsManager(),
+ Server::get(ICommentsManager::class),
$userManager,
- \OC::$server->getUserSession(),
+ Server::get(IUserSession::class),
$dispatcher,
$logger
);
- $contactsSharingBackend = \OC::$server->get(\OCA\DAV\CardDAV\Sharing\Backend::class);
+ $contactsSharingBackend = Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class);
+ $config = Server::get(IConfig::class);
- $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class));
+ $pluginManager = new PluginManager(\OC::$server, Server::get(IAppManager::class));
$usersCardDavBackend = new CardDavBackend(
$db,
$userPrincipalBackend,
$userManager,
$dispatcher,
$contactsSharingBackend,
+ $config
);
$usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users');
$usersAddressBookRoot->disableListing = $disableListing;
@@ -165,6 +152,7 @@ class RootCollection extends SimpleCollection {
$userManager,
$dispatcher,
$contactsSharingBackend,
+ $config
);
$systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system');
$systemAddressBookRoot->disableListing = $disableListing;
@@ -172,14 +160,18 @@ class RootCollection extends SimpleCollection {
$uploadCollection = new Upload\RootCollection(
$userPrincipalBackend,
'principals/users',
- \OC::$server->query(CleanupService::class));
+ Server::get(CleanupService::class),
+ $rootFolder,
+ $userSession,
+ $shareManager,
+ );
$uploadCollection->disableListing = $disableListing;
$avatarCollection = new Avatars\RootCollection($userPrincipalBackend, 'principals/users');
$avatarCollection->disableListing = $disableListing;
$appleProvisioning = new AppleProvisioningNode(
- \OC::$server->query(ITimeFactory::class));
+ Server::get(ITimeFactory::class));
$children = [
new SimpleCollection('principals', [
diff --git a/apps/dav/lib/Search/ACalendarSearchProvider.php b/apps/dav/lib/Search/ACalendarSearchProvider.php
index 82c15efec64..331d05cb500 100644
--- a/apps/dav/lib/Search/ACalendarSearchProvider.php
+++ b/apps/dav/lib/Search/ACalendarSearchProvider.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Search;
@@ -40,18 +23,6 @@ use Sabre\VObject\Reader;
*/
abstract class ACalendarSearchProvider implements IProvider {
- /** @var IAppManager */
- protected $appManager;
-
- /** @var IL10N */
- protected $l10n;
-
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var CalDavBackend */
- protected $backend;
-
/**
* ACalendarSearchProvider constructor.
*
@@ -60,14 +31,12 @@ abstract class ACalendarSearchProvider implements IProvider {
* @param IURLGenerator $urlGenerator
* @param CalDavBackend $backend
*/
- public function __construct(IAppManager $appManager,
- IL10N $l10n,
- IURLGenerator $urlGenerator,
- CalDavBackend $backend) {
- $this->appManager = $appManager;
- $this->l10n = $l10n;
- $this->urlGenerator = $urlGenerator;
- $this->backend = $backend;
+ public function __construct(
+ protected IAppManager $appManager,
+ protected IL10N $l10n,
+ protected IURLGenerator $urlGenerator,
+ protected CalDavBackend $backend,
+ ) {
}
/**
@@ -81,7 +50,7 @@ abstract class ACalendarSearchProvider implements IProvider {
$calendars = $this->backend->getCalendarsForUser($principalUri);
$calendarsById = [];
foreach ($calendars as $calendar) {
- $calendarsById[(int) $calendar['id']] = $calendar;
+ $calendarsById[(int)$calendar['id']] = $calendar;
}
return $calendarsById;
@@ -98,7 +67,7 @@ abstract class ACalendarSearchProvider implements IProvider {
$subscriptions = $this->backend->getSubscriptionsForUser($principalUri);
$subscriptionsById = [];
foreach ($subscriptions as $subscription) {
- $subscriptionsById[(int) $subscription['id']] = $subscription;
+ $subscriptionsById[(int)$subscription['id']] = $subscription;
}
return $subscriptionsById;
diff --git a/apps/dav/lib/Search/ContactsSearchProvider.php b/apps/dav/lib/Search/ContactsSearchProvider.php
index 2a0e70bf045..158c0d0813e 100644
--- a/apps/dav/lib/Search/ContactsSearchProvider.php
+++ b/apps/dav/lib/Search/ContactsSearchProvider.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Search;
@@ -100,7 +81,7 @@ class ContactsSearchProvider implements IFilteringProvider {
$addressBooks = $this->backend->getAddressBooksForUser($principalUri);
$addressBooksById = [];
foreach ($addressBooks as $addressBook) {
- $addressBooksById[(int) $addressBook['id']] = $addressBook;
+ $addressBooksById[(int)$addressBook['id']] = $addressBook;
}
$searchResults = $this->backend->searchPrincipalUri(
@@ -128,9 +109,14 @@ class ContactsSearchProvider implements IFilteringProvider {
$title = (string)$vCard->FN;
$subline = $this->generateSubline($vCard);
- $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);
+ $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string)$vCard->UID);
- return new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
+ $result = new SearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
+ $result->addAttribute('displayName', $title);
+ $result->addAttribute('email', $subline);
+ $result->addAttribute('phoneNumber', (string)$vCard->TEL);
+
+ return $result;
}, $searchResults);
return SearchResult::paginated(
diff --git a/apps/dav/lib/Search/EventsSearchProvider.php b/apps/dav/lib/Search/EventsSearchProvider.php
index dd87f486a1d..55fba40918a 100644
--- a/apps/dav/lib/Search/EventsSearchProvider.php
+++ b/apps/dav/lib/Search/EventsSearchProvider.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Search;
@@ -36,6 +17,7 @@ use OCP\Search\SearchResultEntry;
use Sabre\VObject\Component;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Property;
+use Sabre\VObject\Property\ICalendar\DateTime;
use function array_combine;
use function array_fill;
use function array_key_exists;
@@ -176,8 +158,16 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering
$calendar = $subscriptionsById[$eventRow['calendarid']];
}
$resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']);
+ $result = new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
+
+ $dtStart = $component->DTSTART;
- return new SearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
+ if ($dtStart instanceof DateTime) {
+ $startDateTime = $dtStart->getDateTime()->format('U');
+ $result->addAttribute('createdAt', $startDateTime);
+ }
+
+ return $result;
}, $searchResults);
return SearchResult::paginated(
@@ -204,7 +194,7 @@ class EventsSearchProvider extends ACalendarSearchProvider implements IFiltering
protected function getDavUrlForCalendarObject(
string $principalUri,
string $calendarUri,
- string $calendarObjectUri
+ string $calendarObjectUri,
): string {
[,, $principalId] = explode('/', $principalUri, 3);
diff --git a/apps/dav/lib/Search/TasksSearchProvider.php b/apps/dav/lib/Search/TasksSearchProvider.php
index 5cb64556457..15baf070e81 100644
--- a/apps/dav/lib/Search/TasksSearchProvider.php
+++ b/apps/dav/lib/Search/TasksSearchProvider.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Search;
@@ -141,7 +121,7 @@ class TasksSearchProvider extends ACalendarSearchProvider {
): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('tasks.page.index')
- . '#/calendars/'
+ . 'calendars/'
. $calendarUri
. '/tasks/'
. $taskUri
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index deee381d24c..a92e162f1b0 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -1,51 +1,34 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2022 Informatyka Boguslawski sp. z o.o. sp.k., http://www.ib.pl/
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Brandon Kirsch <brandonkirsch@github.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@artificial-owl.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV;
+use OC\Files\Filesystem;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\BulkUpload\BulkUploadPlugin;
+use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin;
use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\EventComparisonService;
+use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipPlugin;
+use OCA\DAV\CalDAV\Schedule\IMipService;
use OCA\DAV\CalDAV\Security\RateLimitingPlugin;
+use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin;
use OCA\DAV\CardDAV\HasPhotoPlugin;
use OCA\DAV\CardDAV\ImageExportPlugin;
use OCA\DAV\CardDAV\MultiGetExportPlugin;
use OCA\DAV\CardDAV\PhotoCache;
+use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
+use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin;
use OCA\DAV\Comments\CommentsPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
+use OCA\DAV\Connector\Sabre\AppleQuirksPlugin;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\BearerAuth;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
@@ -55,34 +38,63 @@ use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin;
use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
use OCA\DAV\Connector\Sabre\FakeLockerPlugin;
use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\DAV\Connector\Sabre\FilesReportPlugin;
+use OCA\DAV\Connector\Sabre\LockPlugin;
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin;
+use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin;
use OCA\DAV\Connector\Sabre\QuotaPlugin;
use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin;
use OCA\DAV\Connector\Sabre\SharesPlugin;
use OCA\DAV\Connector\Sabre\TagsPlugin;
+use OCA\DAV\Connector\Sabre\ZipFolderPlugin;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Events\SabrePluginAddEvent;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\Files\BrowserErrorPagePlugin;
+use OCA\DAV\Files\FileSearchBackend;
use OCA\DAV\Files\LazySearchBackend;
+use OCA\DAV\Paginate\PaginatePlugin;
use OCA\DAV\Profiler\ProfilerPlugin;
use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin;
use OCA\DAV\SystemTag\SystemTagPlugin;
use OCA\DAV\Upload\ChunkingPlugin;
use OCA\DAV\Upload\ChunkingV2Plugin;
+use OCA\DAV\Upload\UploadAutoMkcolPlugin;
+use OCA\Theming\ThemingDefaults;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
use OCP\AppFramework\Http\Response;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Comments\ICommentsManager;
+use OCP\Defaults;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IFilenameValidator;
+use OCP\Files\IRootFolder;
use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\IAppConfig;
use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IPreview;
use OCP\IRequest;
+use OCP\ISession;
+use OCP\ITagManager;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\Mail\IMailer;
use OCP\Profiler\IProfiler;
use OCP\SabrePluginEvent;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use Psr\Log\LoggerInterface;
use Sabre\CardDAV\VCFExportPlugin;
use Sabre\DAV\Auth\Plugin;
@@ -90,40 +102,40 @@ use Sabre\DAV\UUIDUtil;
use SearchDAV\DAV\SearchPlugin;
class Server {
- private IRequest $request;
- private string $baseUri;
public Connector\Sabre\Server $server;
private IProfiler $profiler;
- public function __construct(IRequest $request, string $baseUri) {
- $this->profiler = \OC::$server->get(IProfiler::class);
+ public function __construct(
+ private IRequest $request,
+ private string $baseUri,
+ ) {
+ $debugEnabled = \OCP\Server::get(IConfig::class)->getSystemValue('debug', false);
+ $this->profiler = \OCP\Server::get(IProfiler::class);
if ($this->profiler->isEnabled()) {
/** @var IEventLogger $eventLogger */
- $eventLogger = \OC::$server->get(IEventLogger::class);
+ $eventLogger = \OCP\Server::get(IEventLogger::class);
$eventLogger->start('runtime', 'DAV Runtime');
}
- $this->request = $request;
- $this->baseUri = $baseUri;
- $logger = \OC::$server->get(LoggerInterface::class);
- /** @var IEventDispatcher $dispatcher */
- $dispatcher = \OC::$server->get(IEventDispatcher::class);
+ $logger = \OCP\Server::get(LoggerInterface::class);
+ $eventDispatcher = \OCP\Server::get(IEventDispatcher::class);
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
+ $this->server->setLogger($logger);
// Add maintenance plugin
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
+ $this->server->addPlugin(new MaintenancePlugin(\OCP\Server::get(IConfig::class), \OC::$server->getL10N('dav')));
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\AppleQuirksPlugin());
+ $this->server->addPlugin(new AppleQuirksPlugin());
// Backends
$authBackend = new Auth(
- \OC::$server->getSession(),
- \OC::$server->getUserSession(),
- \OC::$server->getRequest(),
- \OC::$server->getTwoFactorAuthManager(),
- \OC::$server->getBruteForceThrottler()
+ \OCP\Server::get(ISession::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IRequest::class),
+ \OCP\Server::get(\OC\Authentication\TwoFactorAuth\Manager::class),
+ \OCP\Server::get(IThrottler::class)
);
// Set URL explicitly due to reverse-proxy situations
@@ -131,7 +143,10 @@ class Server {
$this->server->setBaseUri($this->baseUri);
$this->server->addPlugin(new ProfilerPlugin($this->request));
- $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
+ $this->server->addPlugin(new BlockLegacyClientPlugin(
+ \OCP\Server::get(IConfig::class),
+ \OCP\Server::get(ThemingDefaults::class),
+ ));
$this->server->addPlugin(new AnonymousOptionsPlugin());
$authPlugin = new Plugin();
$authPlugin->addBackend(new PublicAuth());
@@ -139,29 +154,32 @@ class Server {
// allow setup of additional auth backends
$event = new SabrePluginEvent($this->server);
- $dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
+ $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
$newAuthEvent = new SabrePluginAuthInitEvent($this->server);
- $dispatcher->dispatchTyped($newAuthEvent);
+ $eventDispatcher->dispatchTyped($newAuthEvent);
$bearerAuthBackend = new BearerAuth(
- \OC::$server->getUserSession(),
- \OC::$server->getSession(),
- \OC::$server->getRequest()
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(ISession::class),
+ \OCP\Server::get(IRequest::class),
+ \OCP\Server::get(IConfig::class),
);
$authPlugin->addBackend($bearerAuthBackend);
// because we are throwing exceptions this plugin has to be the last one
$authPlugin->addBackend($authBackend);
// debugging
- if (\OC::$server->getConfig()->getSystemValue('debug', false)) {
+ if ($debugEnabled) {
+ $this->server->debugEnabled = true;
+ $this->server->addPlugin(new PropFindMonitorPlugin());
$this->server->addPlugin(new \Sabre\DAV\Browser\Plugin());
} else {
$this->server->addPlugin(new DummyGetResponsePlugin());
}
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger));
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
+ $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
+ $this->server->addPlugin(new LockPlugin());
$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// acl
@@ -176,64 +194,69 @@ class Server {
// calendar plugins
if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) {
+ $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
- $this->server->addPlugin(new \OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin(\OC::$server->getConfig(), $logger));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class)));
- if (\OC::$server->getConfig()->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') {
- $this->server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class));
- }
+ $this->server->addPlugin(new ICSExportPlugin(\OCP\Server::get(IConfig::class), $logger));
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OCP\Server::get(IConfig::class), \OCP\Server::get(LoggerInterface::class), \OCP\Server::get(DefaultCalendarValidator::class)));
- $this->server->addPlugin(\OC::$server->get(\OCA\DAV\CalDAV\Trashbin\Plugin::class));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($request));
- if (\OC::$server->getConfig()->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') {
+ $this->server->addPlugin(\OCP\Server::get(\OCA\DAV\CalDAV\Trashbin\Plugin::class));
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($this->request));
+ if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'allow_calendar_link_subscriptions', 'yes') === 'yes') {
$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
}
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
- $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig()));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin(
- \OC::$server->getConfig(),
- \OC::$server->getURLGenerator()
+ $this->server->addPlugin(new PublishPlugin(
+ \OCP\Server::get(IConfig::class),
+ \OCP\Server::get(IURLGenerator::class)
));
$this->server->addPlugin(\OCP\Server::get(RateLimitingPlugin::class));
+ $this->server->addPlugin(\OCP\Server::get(CalDavValidatePlugin::class));
}
// addressbook plugins
if ($this->requestIsForSubtree(['addressbooks', 'principals'])) {
- $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig()));
+ $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OCP\Server::get(IRequest::class), \OCP\Server::get(IConfig::class)));
$this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
$this->server->addPlugin(new VCFExportPlugin());
$this->server->addPlugin(new MultiGetExportPlugin());
$this->server->addPlugin(new HasPhotoPlugin());
- $this->server->addPlugin(new ImageExportPlugin(new PhotoCache(
- \OC::$server->getAppDataDir('dav-photocache'),
- $logger)
- ));
+ $this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class)));
+
+ $this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class));
+ $this->server->addPlugin(\OCP\Server::get(CardDavValidatePlugin::class));
}
// system tags plugins
- $this->server->addPlugin(\OC::$server->get(SystemTagPlugin::class));
+ $this->server->addPlugin(\OCP\Server::get(SystemTagPlugin::class));
// comments plugin
$this->server->addPlugin(new CommentsPlugin(
- \OC::$server->getCommentsManager(),
- \OC::$server->getUserSession()
+ \OCP\Server::get(ICommentsManager::class),
+ \OCP\Server::get(IUserSession::class)
));
$this->server->addPlugin(new CopyEtagHeaderPlugin());
- $this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class)));
+ $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class)));
+ $this->server->addPlugin(new UploadAutoMkcolPlugin());
$this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class)));
$this->server->addPlugin(new ChunkingPlugin());
+ $this->server->addPlugin(new ZipFolderPlugin(
+ $this->server->tree,
+ $logger,
+ $eventDispatcher,
+ ));
+ $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class));
// allow setup of additional plugins
- $dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
+ $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
$typedEvent = new SabrePluginAddEvent($this->server);
- $dispatcher->dispatchTyped($typedEvent);
+ $eventDispatcher->dispatchTyped($typedEvent);
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
// we do not provide locking we emulate it using a fake locking plugin.
- if ($request->isUserAgent([
+ if ($this->request->isUserAgent([
'/WebDAVFS/',
'/OneNote/',
'/^Microsoft-WebDAV/',// Microsoft-WebDAV-MiniRedir/6.1.7601
@@ -249,26 +272,29 @@ class Server {
$this->server->addPlugin(new SearchPlugin($lazySearchBackend));
// wait with registering these until auth is handled and the filesystem is setup
- $this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger) {
+ $this->server->on('beforeMethod:*', function () use ($root, $lazySearchBackend, $logger): void {
// Allow view-only plugin for webdav requests
$this->server->addPlugin(new ViewOnlyPlugin(
\OC::$server->getUserFolder(),
));
// custom properties plugin must be the last one
- $userSession = \OC::$server->getUserSession();
+ $userSession = \OCP\Server::get(IUserSession::class);
$user = $userSession->getUser();
if ($user !== null) {
- $view = \OC\Files\Filesystem::getView();
+ $view = Filesystem::getView();
+ $config = \OCP\Server::get(IConfig::class);
$this->server->addPlugin(
new FilesPlugin(
$this->server->tree,
- \OC::$server->getConfig(),
+ $config,
$this->request,
- \OC::$server->getPreviewManager(),
- \OC::$server->getUserSession(),
+ \OCP\Server::get(IPreview::class),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IFilenameValidator::class),
+ \OCP\Server::get(IAccountManager::class),
false,
- !\OC::$server->getConfig()->getSystemValue('debug', false)
+ $config->getSystemValueBool('debug', false) === false,
)
);
$this->server->addPlugin(new ChecksumUpdatePlugin());
@@ -278,8 +304,9 @@ class Server {
new CustomPropertiesBackend(
$this->server,
$this->server->tree,
- \OC::$server->getDatabaseConnection(),
- \OC::$server->getUserSession()->getUser()
+ \OCP\Server::get(IDBConnection::class),
+ \OCP\Server::get(IUserSession::class)->getUser(),
+ \OCP\Server::get(DefaultCalendarValidator::class),
)
)
);
@@ -289,39 +316,55 @@ class Server {
}
$this->server->addPlugin(
new TagsPlugin(
- $this->server->tree, \OC::$server->getTagManager()
+ $this->server->tree, \OCP\Server::get(ITagManager::class), \OCP\Server::get(IEventDispatcher::class), \OCP\Server::get(IUserSession::class)
)
);
+
// TODO: switch to LazyUserFolder
$userFolder = \OC::$server->getUserFolder();
+ $shareManager = \OCP\Server::get(\OCP\Share\IManager::class);
$this->server->addPlugin(new SharesPlugin(
$this->server->tree,
$userSession,
$userFolder,
- \OC::$server->getShareManager()
+ $shareManager,
));
$this->server->addPlugin(new CommentPropertiesPlugin(
- \OC::$server->getCommentsManager(),
+ \OCP\Server::get(ICommentsManager::class),
$userSession
));
+ if (\OCP\Server::get(IConfig::class)->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') {
+ $this->server->addPlugin(new IMipPlugin(
+ \OCP\Server::get(IAppConfig::class),
+ \OCP\Server::get(IMailer::class),
+ \OCP\Server::get(LoggerInterface::class),
+ \OCP\Server::get(ITimeFactory::class),
+ \OCP\Server::get(Defaults::class),
+ $userSession,
+ \OCP\Server::get(IMipService::class),
+ \OCP\Server::get(EventComparisonService::class),
+ \OCP\Server::get(\OCP\Mail\Provider\IManager::class)
+ ));
+ }
$this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin());
if ($view !== null) {
$this->server->addPlugin(new FilesReportPlugin(
$this->server->tree,
$view,
- \OC::$server->getSystemTagManager(),
- \OC::$server->getSystemTagObjectMapper(),
- \OC::$server->getTagManager(),
+ \OCP\Server::get(ISystemTagManager::class),
+ \OCP\Server::get(ISystemTagObjectMapper::class),
+ \OCP\Server::get(ITagManager::class),
$userSession,
- \OC::$server->getGroupManager(),
+ \OCP\Server::get(IGroupManager::class),
$userFolder,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
));
- $lazySearchBackend->setBackend(new \OCA\DAV\Files\FileSearchBackend(
+ $lazySearchBackend->setBackend(new FileSearchBackend(
+ $this->server,
$this->server->tree,
$user,
- \OC::$server->getRootFolder(),
- \OC::$server->getShareManager(),
+ \OCP\Server::get(IRootFolder::class),
+ $shareManager,
$view,
\OCP\Server::get(IFilesMetadataManager::class)
));
@@ -332,16 +375,16 @@ class Server {
)
);
}
- $this->server->addPlugin(new \OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin(
- \OC::$server->getConfig(),
- \OC::$server->query(BirthdayService::class),
+ $this->server->addPlugin(new EnablePlugin(
+ \OCP\Server::get(IConfig::class),
+ \OCP\Server::get(BirthdayService::class),
$user
));
$this->server->addPlugin(new AppleProvisioningPlugin(
- \OC::$server->getUserSession(),
- \OC::$server->getURLGenerator(),
- \OC::$server->getThemingDefaults(),
- \OC::$server->getRequest(),
+ \OCP\Server::get(IUserSession::class),
+ \OCP\Server::get(IURLGenerator::class),
+ \OCP\Server::get(ThemingDefaults::class),
+ \OCP\Server::get(IRequest::class),
\OC::$server->getL10N('dav'),
function () {
return UUIDUtil::getUUID();
@@ -352,7 +395,7 @@ class Server {
// register plugins from apps
$pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ \OCP\Server::get(IAppManager::class)
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$this->server->addPlugin($appPlugin);
@@ -369,13 +412,13 @@ class Server {
public function exec() {
/** @var IEventLogger $eventLogger */
- $eventLogger = \OC::$server->get(IEventLogger::class);
+ $eventLogger = \OCP\Server::get(IEventLogger::class);
$eventLogger->start('dav_server_exec', '');
- $this->server->exec();
+ $this->server->start();
$eventLogger->end('dav_server_exec');
if ($this->profiler->isEnabled()) {
$eventLogger->end('runtime');
- $profile = $this->profiler->collect(\OC::$server->get(IRequest::class), new Response());
+ $profile = $this->profiler->collect(\OCP\Server::get(IRequest::class), new Response());
$this->profiler->saveProfile($profile);
}
}
diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php
index 7dc74f7d6ae..f632ee6015d 100644
--- a/apps/dav/lib/ServerFactory.php
+++ b/apps/dav/lib/ServerFactory.php
@@ -3,33 +3,22 @@
declare(strict_types=1);
/**
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCA\DAV\Connector\Sabre\Server;
class ServerFactory {
public function createInviationResponseServer(bool $public): InvitationResponseServer {
return new InvitationResponseServer(false);
}
+
+ public function createAttendeeAvailabilityServer(): Server {
+ return (new InvitationResponseServer(false))->getServer();
+ }
}
diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php
index 3e2a218d52b..7cbc0386d43 100644
--- a/apps/dav/lib/Service/AbsenceService.php
+++ b/apps/dav/lib/Service/AbsenceService.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Service;
@@ -64,6 +47,8 @@ class AbsenceService {
string $lastDay,
string $status,
string $message,
+ ?string $replacementUserId = null,
+ ?string $replacementUserDisplayName = null,
): Absence {
try {
$absence = $this->absenceMapper->findByUserId($user->getUID());
@@ -76,6 +61,8 @@ class AbsenceService {
$absence->setLastDay($lastDay);
$absence->setStatus($status);
$absence->setMessage($message);
+ $absence->setReplacementUserId($replacementUserId);
+ $absence->setReplacementUserDisplayName($replacementUserDisplayName);
if ($absence->getId() === null) {
$absence = $this->absenceMapper->insert($absence);
diff --git a/apps/dav/lib/Service/ExampleContactService.php b/apps/dav/lib/Service/ExampleContactService.php
new file mode 100644
index 00000000000..6ed6c66cbb3
--- /dev/null
+++ b/apps/dav/lib/Service/ExampleContactService.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\AppFramework\Services\IAppConfig;
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Uid\Uuid;
+
+class ExampleContactService {
+ private readonly IAppData $appData;
+
+ public function __construct(
+ IAppDataFactory $appDataFactory,
+ private readonly IAppConfig $appConfig,
+ private readonly LoggerInterface $logger,
+ private readonly CardDavBackend $cardDav,
+ ) {
+ $this->appData = $appDataFactory->get(Application::APP_ID);
+ }
+
+ public function isDefaultContactEnabled(): bool {
+ return $this->appConfig->getAppValueBool('enableDefaultContact', true);
+ }
+
+ public function setDefaultContactEnabled(bool $value): void {
+ $this->appConfig->setAppValueBool('enableDefaultContact', $value);
+ }
+
+ public function getCard(): ?string {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ return null;
+ }
+
+ if (!$folder->fileExists('defaultContact.vcf')) {
+ return null;
+ }
+
+ return $folder->getFile('defaultContact.vcf')->getContent();
+ }
+
+ public function setCard(?string $cardData = null) {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ $folder = $this->appData->newFolder('defaultContact');
+ }
+
+ $isCustom = true;
+ if (is_null($cardData)) {
+ $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf');
+ $isCustom = false;
+ }
+
+ if (!$cardData) {
+ throw new \Exception('Could not read exampleContact.vcf');
+ }
+
+ $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf');
+ $file->putContent($cardData);
+
+ $this->appConfig->setAppValueBool('hasCustomDefaultContact', $isCustom);
+ }
+
+ public function defaultContactExists(): bool {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ return false;
+ }
+ return $folder->fileExists('defaultContact.vcf');
+ }
+
+ public function createDefaultContact(int $addressBookId): void {
+ if (!$this->isDefaultContactEnabled()) {
+ return;
+ }
+
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ $defaultContactFile = $folder->getFile('defaultContact.vcf');
+ $data = $defaultContactFile->getContent();
+ } catch (\Exception $e) {
+ $this->logger->error('Couldn\'t get default contact file', ['exception' => $e]);
+ return;
+ }
+
+ // Make sure the UID is unique
+ $newUid = Uuid::v4()->toRfc4122();
+ $newRev = date('Ymd\THis\Z');
+ $vcard = \Sabre\VObject\Reader::read($data, \Sabre\VObject\Reader::OPTION_FORGIVING);
+ if ($vcard->UID) {
+ $vcard->UID->setValue($newUid);
+ } else {
+ $vcard->add('UID', $newUid);
+ }
+ if ($vcard->REV) {
+ $vcard->REV->setValue($newRev);
+ } else {
+ $vcard->add('REV', $newRev);
+ }
+
+ // Level 3 means that the document is invalid
+ // https://sabre.io/vobject/vcard/#validating-vcard
+ $level3Warnings = array_filter($vcard->validate(), static function ($warning) {
+ return $warning['level'] === 3;
+ });
+
+ if (!empty($level3Warnings)) {
+ $this->logger->error('Default contact is invalid', ['warnings' => $level3Warnings]);
+ return;
+ }
+ try {
+ $this->cardDav->createCard($addressBookId, 'default', $vcard->serialize(), false);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+}
diff --git a/apps/dav/lib/Service/ExampleEventService.php b/apps/dav/lib/Service/ExampleEventService.php
new file mode 100644
index 00000000000..3b2b07fe416
--- /dev/null
+++ b/apps/dav/lib/Service/ExampleEventService.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Exception\ExampleEventException;
+use OCA\DAV\Model\ExampleEvent;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IAppConfig;
+use OCP\IL10N;
+use OCP\Security\ISecureRandom;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+
+class ExampleEventService {
+ private const FOLDER_NAME = 'example_event';
+ private const FILE_NAME = 'example_event.ics';
+ private const ENABLE_CONFIG_KEY = 'create_example_event';
+
+ public function __construct(
+ private readonly CalDavBackend $calDavBackend,
+ private readonly ISecureRandom $random,
+ private readonly ITimeFactory $time,
+ private readonly IAppData $appData,
+ private readonly IAppConfig $appConfig,
+ private readonly IL10N $l10n,
+ ) {
+ }
+
+ public function createExampleEvent(int $calendarId): void {
+ if (!$this->shouldCreateExampleEvent()) {
+ return;
+ }
+
+ $exampleEvent = $this->getExampleEvent();
+ $uid = $exampleEvent->getUid();
+ $this->calDavBackend->createCalendarObject(
+ $calendarId,
+ "$uid.ics",
+ $exampleEvent->getIcs(),
+ );
+ }
+
+ private function getStartDate(): \DateTimeInterface {
+ return $this->time->now()
+ ->add(new \DateInterval('P7D'))
+ ->setTime(10, 00);
+ }
+
+ private function getEndDate(): \DateTimeInterface {
+ return $this->time->now()
+ ->add(new \DateInterval('P7D'))
+ ->setTime(11, 00);
+ }
+
+ private function getDefaultEvent(string $uid): VCalendar {
+ $defaultDescription = $this->l10n->t(<<<EOF
+Welcome to Nextcloud Calendar!
+
+This is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!
+
+With Nextcloud Calendar, you can:
+- Create, edit, and manage events effortlessly.
+- Create multiple calendars and share them with teammates, friends, or family.
+- Check availability and display your busy times to others.
+- Seamlessly integrate with apps and devices via CalDAV.
+- Customize your experience: schedule recurring events, adjust notifications and other settings.
+EOF);
+
+ $vCalendar = new VCalendar();
+ $props = [
+ 'UID' => $uid,
+ 'DTSTAMP' => $this->time->now(),
+ 'SUMMARY' => $this->l10n->t('Example event - open me!'),
+ 'DTSTART' => $this->getStartDate(),
+ 'DTEND' => $this->getEndDate(),
+ 'DESCRIPTION' => $defaultDescription,
+ ];
+ $vCalendar->add('VEVENT', $props);
+ return $vCalendar;
+ }
+
+ /**
+ * @return string|null The ics of the custom example event or null if no custom event was uploaded.
+ * @throws ExampleEventException If reading the custom ics file fails.
+ */
+ private function getCustomExampleEvent(): ?string {
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ $icsFile = $folder->getFile(self::FILE_NAME);
+ } catch (NotFoundException $e) {
+ return null;
+ }
+
+ try {
+ return $icsFile->getContent();
+ } catch (NotFoundException|NotPermittedException $e) {
+ throw new ExampleEventException(
+ 'Failed to read custom example event',
+ 0,
+ $e,
+ );
+ }
+ }
+
+ /**
+ * Get the configured example event or the default one.
+ *
+ * @throws ExampleEventException If loading the custom example event fails.
+ */
+ public function getExampleEvent(): ExampleEvent {
+ $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
+ $customIcs = $this->getCustomExampleEvent();
+ if ($customIcs === null) {
+ return new ExampleEvent($this->getDefaultEvent($uid), $uid);
+ }
+
+ [$vCalendar, $vEvent] = $this->parseEvent($customIcs);
+ $vEvent->UID = $uid;
+ $vEvent->DTSTART = $this->getStartDate();
+ $vEvent->DTEND = $this->getEndDate();
+ $vEvent->remove('ORGANIZER');
+ $vEvent->remove('ATTENDEE');
+ return new ExampleEvent($vCalendar, $uid);
+ }
+
+ /**
+ * @psalm-return list{VCalendar, VEvent} The VCALENDAR document and its VEVENT child component
+ * @throws ExampleEventException If parsing the event fails or if it is invalid.
+ */
+ private function parseEvent(string $ics): array {
+ try {
+ $vCalendar = \Sabre\VObject\Reader::read($ics);
+ if (!($vCalendar instanceof VCalendar)) {
+ throw new ExampleEventException('Custom event does not contain a VCALENDAR component');
+ }
+
+ /** @var VEvent|null $vEvent */
+ $vEvent = $vCalendar->getBaseComponent('VEVENT');
+ if ($vEvent === null) {
+ throw new ExampleEventException('Custom event does not contain a VEVENT component');
+ }
+ } catch (\Exception $e) {
+ throw new ExampleEventException('Failed to parse custom event: ' . $e->getMessage(), 0, $e);
+ }
+
+ return [$vCalendar, $vEvent];
+ }
+
+ public function saveCustomExampleEvent(string $ics): void {
+ // Parse and validate the event before attempting to save it to prevent run time errors
+ $this->parseEvent($ics);
+
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ } catch (NotFoundException $e) {
+ $folder = $this->appData->newFolder(self::FOLDER_NAME);
+ }
+
+ try {
+ $existingFile = $folder->getFile(self::FILE_NAME);
+ $existingFile->putContent($ics);
+ } catch (NotFoundException $e) {
+ $folder->newFile(self::FILE_NAME, $ics);
+ }
+ }
+
+ public function deleteCustomExampleEvent(): void {
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ $file = $folder->getFile(self::FILE_NAME);
+ } catch (NotFoundException $e) {
+ return;
+ }
+
+ $file->delete();
+ }
+
+ public function hasCustomExampleEvent(): bool {
+ try {
+ return $this->getCustomExampleEvent() !== null;
+ } catch (ExampleEventException $e) {
+ return false;
+ }
+ }
+
+ public function setCreateExampleEvent(bool $enable): void {
+ $this->appConfig->setValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, $enable);
+ }
+
+ public function shouldCreateExampleEvent(): bool {
+ return $this->appConfig->getValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, true);
+ }
+}
diff --git a/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php b/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php
new file mode 100644
index 00000000000..2f7b9f8fcc9
--- /dev/null
+++ b/apps/dav/lib/Settings/Admin/SystemAddressBookSettings.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Settings\Admin;
+
+use OCP\IL10N;
+use OCP\Settings\DeclarativeSettingsTypes;
+use OCP\Settings\IDeclarativeSettingsForm;
+
+class SystemAddressBookSettings implements IDeclarativeSettingsForm {
+
+ public function __construct(
+ private IL10N $l,
+ ) {
+ }
+
+ public function getSchema(): array {
+ return [
+ 'id' => 'dav-admin-system-address-book',
+ 'priority' => 10,
+ 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN,
+ 'section_id' => 'groupware',
+ 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL,
+ 'title' => $this->l->t('System Address Book'),
+ 'description' => $this->l->t('The system address book contains contact information for all users in your instance.'),
+
+ 'fields' => [
+ [
+ 'id' => 'system_addressbook_enabled',
+ 'title' => $this->l->t('Enable System Address Book'),
+ 'type' => DeclarativeSettingsTypes::CHECKBOX,
+ 'default' => false,
+ 'options' => [],
+ ],
+ ],
+ ];
+ }
+
+}
diff --git a/apps/dav/lib/Settings/AvailabilitySettings.php b/apps/dav/lib/Settings/AvailabilitySettings.php
index f8986ffe5d1..a1ada96b255 100644
--- a/apps/dav/lib/Settings/AvailabilitySettings.php
+++ b/apps/dav/lib/Settings/AvailabilitySettings.php
@@ -2,26 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Settings;
@@ -37,19 +20,14 @@ use OCP\User\IAvailabilityCoordinator;
use Psr\Log\LoggerInterface;
class AvailabilitySettings implements ISettings {
- protected IConfig $config;
- protected IInitialState $initialState;
- protected ?string $userId;
-
- public function __construct(IConfig $config,
- IInitialState $initialState,
- ?string $userId,
+ public function __construct(
+ protected IConfig $config,
+ protected IInitialState $initialState,
+ protected ?string $userId,
private LoggerInterface $logger,
private IAvailabilityCoordinator $coordinator,
- private AbsenceMapper $absenceMapper) {
- $this->config = $config;
- $this->initialState = $initialState;
- $this->userId = $userId;
+ private AbsenceMapper $absenceMapper,
+ ) {
}
public function getForm(): TemplateResponse {
diff --git a/apps/dav/lib/Settings/CalDAVSettings.php b/apps/dav/lib/Settings/CalDAVSettings.php
index 2985c9fc888..5e19539a899 100644
--- a/apps/dav/lib/Settings/CalDAVSettings.php
+++ b/apps/dav/lib/Settings/CalDAVSettings.php
@@ -1,31 +1,13 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author François Freitag <mail@franek.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Settings;
use OCA\DAV\AppInfo\Application;
+use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
@@ -34,14 +16,6 @@ use OCP\Settings\IDelegatedSettings;
class CalDAVSettings implements IDelegatedSettings {
- /** @var IConfig */
- private $config;
-
- /** @var IInitialState */
- private $initialState;
-
- private IURLGenerator $urlGenerator;
-
private const defaults = [
'sendInvitations' => 'yes',
'generateBirthdayCalendar' => 'yes',
@@ -56,10 +30,12 @@ class CalDAVSettings implements IDelegatedSettings {
* @param IConfig $config
* @param IInitialState $initialState
*/
- public function __construct(IConfig $config, IInitialState $initialState, IURLGenerator $urlGenerator) {
- $this->config = $config;
- $this->initialState = $initialState;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ private IConfig $config,
+ private IInitialState $initialState,
+ private IURLGenerator $urlGenerator,
+ private IAppManager $appManager,
+ ) {
}
public function getForm(): TemplateResponse {
@@ -71,10 +47,11 @@ class CalDAVSettings implements IDelegatedSettings {
return new TemplateResponse(Application::APP_ID, 'settings-admin-caldav');
}
- /**
- * @return string
- */
- public function getSection() {
+ public function getSection(): ?string {
+ if (!$this->appManager->isBackendRequired(IAppManager::BACKEND_CALDAV)) {
+ return null;
+ }
+
return 'groupware';
}
diff --git a/apps/dav/lib/Settings/ExampleContentSettings.php b/apps/dav/lib/Settings/ExampleContentSettings.php
new file mode 100644
index 00000000000..7b6f9b03a3a
--- /dev/null
+++ b/apps/dav/lib/Settings/ExampleContentSettings.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Settings;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IAppConfig;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\Settings\ISettings;
+
+class ExampleContentSettings implements ISettings {
+ public function __construct(
+ private readonly IAppConfig $appConfig,
+ private readonly IInitialState $initialState,
+ private readonly IAppManager $appManager,
+ private readonly ExampleEventService $exampleEventService,
+ private readonly ExampleContactService $exampleContactService,
+ ) {
+ }
+
+ public function getForm(): TemplateResponse {
+ $calendarEnabled = $this->appManager->isEnabledForUser('calendar');
+ $contactsEnabled = $this->appManager->isEnabledForUser('contacts');
+ $this->initialState->provideInitialState('calendarEnabled', $calendarEnabled);
+ $this->initialState->provideInitialState('contactsEnabled', $contactsEnabled);
+
+ if ($calendarEnabled) {
+ $enableDefaultEvent = $this->exampleEventService->shouldCreateExampleEvent();
+ $this->initialState->provideInitialState('create_example_event', $enableDefaultEvent);
+ $this->initialState->provideInitialState(
+ 'has_custom_example_event',
+ $this->exampleEventService->hasCustomExampleEvent(),
+ );
+ }
+
+ if ($contactsEnabled) {
+ $this->initialState->provideInitialState(
+ 'enableDefaultContact',
+ $this->exampleContactService->isDefaultContactEnabled(),
+ );
+ $this->initialState->provideInitialState(
+ 'hasCustomDefaultContact',
+ $this->appConfig->getAppValueBool('hasCustomDefaultContact'),
+ );
+ }
+
+ return new TemplateResponse(Application::APP_ID, 'settings-example-content');
+ }
+
+ public function getSection(): ?string {
+ if (!$this->appManager->isEnabledForUser('contacts')
+ && !$this->appManager->isEnabledForUser('calendar')) {
+ return null;
+ }
+
+ return 'groupware';
+ }
+
+ public function getPriority(): int {
+ return 10;
+ }
+}
diff --git a/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php b/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php
index 32c6fb06393..c3f0742a640 100644
--- a/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php
+++ b/apps/dav/lib/SetupChecks/NeedsSystemAddressBookSync.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\SetupChecks;
diff --git a/apps/dav/lib/SetupChecks/WebdavEndpoint.php b/apps/dav/lib/SetupChecks/WebdavEndpoint.php
index 72585e93eeb..c2574202fcd 100644
--- a/apps/dav/lib/SetupChecks/WebdavEndpoint.php
+++ b/apps/dav/lib/SetupChecks/WebdavEndpoint.php
@@ -3,35 +3,17 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2024 Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\SetupChecks;
-use OCA\Settings\SetupChecks\CheckServerResponseTrait;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
+use OCP\SetupCheck\CheckServerResponseTrait;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
use Psr\Log\LoggerInterface;
diff --git a/apps/dav/lib/Storage/PublicOwnerWrapper.php b/apps/dav/lib/Storage/PublicOwnerWrapper.php
index 10bcd20de05..a0f1607d971 100644
--- a/apps/dav/lib/Storage/PublicOwnerWrapper.php
+++ b/apps/dav/lib/Storage/PublicOwnerWrapper.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Storage;
@@ -29,27 +12,25 @@ use OC\Files\Storage\Wrapper\Wrapper;
class PublicOwnerWrapper extends Wrapper {
- /** @var string */
- private $owner;
+ private string $owner;
/**
- * @param array $arguments ['storage' => $storage, 'owner' => $owner]
+ * @param array $parameters ['storage' => $storage, 'owner' => $owner]
*
* $storage: The storage the permissions mask should be applied on
* $owner: The owner to use in case no owner is found
*/
- public function __construct($arguments) {
- parent::__construct($arguments);
- $this->owner = $arguments['owner'];
+ public function __construct(array $parameters) {
+ parent::__construct($parameters);
+ $this->owner = $parameters['owner'];
}
- public function getOwner($path) {
+ public function getOwner(string $path): string|false {
$owner = parent::getOwner($path);
-
- if ($owner === null || $owner === false) {
- return $this->owner;
+ if ($owner !== false) {
+ return $owner;
}
- return $owner;
+ return $this->owner;
}
}
diff --git a/apps/dav/lib/Storage/PublicShareWrapper.php b/apps/dav/lib/Storage/PublicShareWrapper.php
new file mode 100644
index 00000000000..fb0db4dca4c
--- /dev/null
+++ b/apps/dav/lib/Storage/PublicShareWrapper.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Storage;
+
+use OC\Files\Storage\Wrapper\Wrapper;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Share\IShare;
+
+class PublicShareWrapper extends Wrapper implements ISharedStorage {
+
+ private IShare $share;
+
+ /**
+ * @param array $parameters ['storage' => $storage, 'share' => $share]
+ *
+ * $storage: The storage the permissions mask should be applied on
+ * $share: The share to use in case no share is found
+ */
+ public function __construct(array $parameters) {
+ parent::__construct($parameters);
+ $this->share = $parameters['share'];
+ }
+
+ public function getShare(): IShare {
+ $storage = parent::getWrapperStorage();
+ if (method_exists($storage, 'getShare')) {
+ /** @var ISharedStorage $storage */
+ return $storage->getShare();
+ }
+
+ return $this->share;
+ }
+}
diff --git a/apps/dav/lib/SystemTag/SystemTagList.php b/apps/dav/lib/SystemTag/SystemTagList.php
index 678c8042a39..b55f10164d7 100644
--- a/apps/dav/lib/SystemTag/SystemTagList.php
+++ b/apps/dav/lib/SystemTag/SystemTagList.php
@@ -1,22 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
@@ -34,16 +20,20 @@ use Sabre\Xml\Writer;
*/
class SystemTagList implements Element {
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
+ private array $canAssignTagMap = [];
- /** @var ISystemTag[] */
- private array $tags;
- private ISystemTagManager $tagManager;
- private IUser $user;
-
- public function __construct(array $tags, ISystemTagManager $tagManager, IUser $user) {
+ /**
+ * @param ISystemTag[] $tags
+ */
+ public function __construct(
+ private array $tags,
+ ISystemTagManager $tagManager,
+ ?IUser $user,
+ ) {
$this->tags = $tags;
- $this->tagManager = $tagManager;
- $this->user = $user;
+ foreach ($this->tags as $tag) {
+ $this->canAssignTagMap[$tag->getId()] = $tagManager->canUserAssignTag($tag, $user);
+ }
}
/**
@@ -61,10 +51,11 @@ class SystemTagList implements Element {
foreach ($this->tags as $tag) {
$writer->startElement('{' . self::NS_NEXTCLOUD . '}system-tag');
$writer->writeAttributes([
- SystemTagPlugin::CANASSIGN_PROPERTYNAME => $this->tagManager->canUserAssignTag($tag, $this->user) ? 'true' : 'false',
+ SystemTagPlugin::CANASSIGN_PROPERTYNAME => $this->canAssignTagMap[$tag->getId()] ? 'true' : 'false',
SystemTagPlugin::ID_PROPERTYNAME => $tag->getId(),
SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME => $tag->isUserAssignable() ? 'true' : 'false',
SystemTagPlugin::USERVISIBLE_PROPERTYNAME => $tag->isUserVisible() ? 'true' : 'false',
+ SystemTagPlugin::COLOR_PROPERTYNAME => $tag->getColor() ?? '',
]);
$writer->write($tag->getName());
$writer->endElement();
diff --git a/apps/dav/lib/SystemTag/SystemTagMappingNode.php b/apps/dav/lib/SystemTag/SystemTagMappingNode.php
index 113c8d82836..12d604a7d6e 100644
--- a/apps/dav/lib/SystemTag/SystemTagMappingNode.php
+++ b/apps/dav/lib/SystemTag/SystemTagMappingNode.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php
index 8ade5085b03..2341d4823ba 100644
--- a/apps/dav/lib/SystemTag/SystemTagNode.php
+++ b/apps/dav/lib/SystemTag/SystemTagNode.php
@@ -1,35 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
use OCP\IUser;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;
-
use OCP\SystemTag\TagNotFoundException;
+use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
@@ -38,31 +22,7 @@ use Sabre\DAV\Exception\NotFound;
/**
* DAV node representing a system tag, with the name being the tag id.
*/
-class SystemTagNode implements \Sabre\DAV\INode {
-
- /**
- * @var ISystemTag
- */
- protected $tag;
-
- /**
- * @var ISystemTagManager
- */
- protected $tagManager;
-
- /**
- * User
- *
- * @var IUser
- */
- protected $user;
-
- /**
- * Whether to allow permissions for admins
- *
- * @var bool
- */
- protected $isAdmin;
+class SystemTagNode implements \Sabre\DAV\ICollection {
protected int $numberOfFiles = -1;
protected int $referenceFileId = -1;
@@ -75,11 +35,19 @@ class SystemTagNode implements \Sabre\DAV\INode {
* @param bool $isAdmin whether to allow operations for admins
* @param ISystemTagManager $tagManager tag manager
*/
- public function __construct(ISystemTag $tag, IUser $user, $isAdmin, ISystemTagManager $tagManager) {
- $this->tag = $tag;
- $this->user = $user;
- $this->isAdmin = $isAdmin;
- $this->tagManager = $tagManager;
+ public function __construct(
+ protected ISystemTag $tag,
+ /**
+ * User
+ */
+ protected IUser $user,
+ /**
+ * Whether to allow permissions for admins
+ */
+ protected bool $isAdmin,
+ protected ISystemTagManager $tagManager,
+ protected ISystemTagObjectMapper $tagMapper,
+ ) {
}
/**
@@ -119,12 +87,13 @@ class SystemTagNode implements \Sabre\DAV\INode {
* @param string $name new tag name
* @param bool $userVisible user visible
* @param bool $userAssignable user assignable
+ * @param string $color color
*
* @throws NotFound whenever the given tag id does not exist
* @throws Forbidden whenever there is no permission to update said tag
* @throws Conflict whenever a tag already exists with the given attributes
*/
- public function update($name, $userVisible, $userAssignable): void {
+ public function update($name, $userVisible, $userAssignable, $color): void {
try {
if (!$this->tagManager->canUserSeeTag($this->tag, $this->user)) {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist');
@@ -143,13 +112,18 @@ class SystemTagNode implements \Sabre\DAV\INode {
}
}
- $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable);
+ // Make sure color is a proper hex
+ if ($color !== null && (strlen($color) !== 6 || !ctype_xdigit($color))) {
+ throw new BadRequest('Color must be a 6-digit hexadecimal value');
+ }
+
+ $this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable, $color);
} catch (TagNotFoundException $e) {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist');
} catch (TagAlreadyExistsException $e) {
throw new Conflict(
- 'Tag with the properties "' . $name . '", ' .
- $userVisible . ', ' . $userAssignable . ' already exists'
+ 'Tag with the properties "' . $name . '", '
+ . $userVisible . ', ' . $userAssignable . ' already exists'
);
}
}
@@ -198,4 +172,31 @@ class SystemTagNode implements \Sabre\DAV\INode {
public function setReferenceFileId(int $referenceFileId): void {
$this->referenceFileId = $referenceFileId;
}
+
+ public function createFile($name, $data = null) {
+ throw new MethodNotAllowed();
+ }
+
+ public function createDirectory($name) {
+ throw new MethodNotAllowed();
+ }
+
+ public function getChild($name) {
+ return new SystemTagObjectType($this->tag, $name, $this->tagManager, $this->tagMapper);
+ }
+
+ public function childExists($name) {
+ $objectTypes = $this->tagMapper->getAvailableObjectTypes();
+ return in_array($name, $objectTypes);
+ }
+
+ public function getChildren() {
+ $objectTypes = $this->tagMapper->getAvailableObjectTypes();
+ return array_map(
+ function ($objectType) {
+ return new SystemTagObjectType($this->tag, $objectType, $this->tagManager, $this->tagMapper);
+ },
+ $objectTypes
+ );
+ }
}
diff --git a/apps/dav/lib/SystemTag/SystemTagObjectType.php b/apps/dav/lib/SystemTag/SystemTagObjectType.php
new file mode 100644
index 00000000000..0d348cd95f4
--- /dev/null
+++ b/apps/dav/lib/SystemTag/SystemTagObjectType.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\DAV\SystemTag;
+
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use Sabre\DAV\Exception\MethodNotAllowed;
+
+/**
+ * SystemTagObjectType property
+ * This property represent a type of object which tags are assigned to.
+ */
+class SystemTagObjectType implements \Sabre\DAV\IFile {
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
+
+ /** @var string[] */
+ private array $objectsIds = [];
+
+ public function __construct(
+ private ISystemTag $tag,
+ private string $type,
+ private ISystemTagManager $tagManager,
+ private ISystemTagObjectMapper $tagMapper,
+ ) {
+ }
+
+ /**
+ * Get the list of object ids that have this tag assigned.
+ */
+ public function getObjectsIds(): array {
+ if (empty($this->objectsIds)) {
+ $this->objectsIds = $this->tagMapper->getObjectIdsForTags($this->tag->getId(), $this->type);
+ }
+
+ return $this->objectsIds;
+ }
+
+ /**
+ * Returns the system tag represented by this node
+ *
+ * @return ISystemTag system tag
+ */
+ public function getSystemTag() {
+ return $this->tag;
+ }
+
+ public function getName() {
+ return $this->type;
+ }
+
+ public function getLastModified() {
+ return null;
+ }
+
+ public function getETag() {
+ return '"' . $this->tag->getETag() . '"';
+ }
+
+ public function setName($name) {
+ throw new MethodNotAllowed();
+ }
+ public function put($data) {
+ throw new MethodNotAllowed();
+ }
+ public function get() {
+ throw new MethodNotAllowed();
+ }
+ public function delete() {
+ throw new MethodNotAllowed();
+ }
+ public function getContentType() {
+ throw new MethodNotAllowed();
+ }
+ public function getSize() {
+ throw new MethodNotAllowed();
+ }
+}
diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php
index 34b71ae40f2..4d4499c7559 100644
--- a/apps/dav/lib/SystemTag/SystemTagPlugin.php
+++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php
@@ -1,32 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\DAV\Connector\Sabre\Node;
+use OCP\AppFramework\Http;
+use OCP\Constants;
+use OCP\Files\IRootFolder;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
@@ -34,6 +20,8 @@ use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;
+use OCP\SystemTag\TagCreationForbiddenException;
+use OCP\SystemTag\TagUpdateForbiddenException;
use OCP\Util;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
@@ -55,6 +43,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id';
public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name';
public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible';
@@ -63,45 +52,27 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
- public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
+ public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
+ public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids';
+ public const COLOR_PROPERTYNAME = '{http://nextcloud.org/ns}color';
/**
* @var \Sabre\DAV\Server $server
*/
private $server;
- /**
- * @var ISystemTagManager
- */
- protected $tagManager;
-
- /**
- * @var IUserSession
- */
- protected $userSession;
-
- /**
- * @var IGroupManager
- */
- protected $groupManager;
-
/** @var array<int, string[]> */
private array $cachedTagMappings = [];
/** @var array<string, ISystemTag> */
private array $cachedTags = [];
- private ISystemTagObjectMapper $tagMapper;
-
public function __construct(
- ISystemTagManager $tagManager,
- IGroupManager $groupManager,
- IUserSession $userSession,
- ISystemTagObjectMapper $tagMapper,
+ protected ISystemTagManager $tagManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IRootFolder $rootFolder,
+ protected ISystemTagObjectMapper $tagMapper,
) {
- $this->tagManager = $tagManager;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->tagMapper = $tagMapper;
}
/**
@@ -117,6 +88,9 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
+ $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';
+
+ $server->xml->elementMap[self::OBJECTIDS_PROPERTYNAME] = SystemTagsObjectList::class;
$server->protectedProperties[] = self::ID_PROPERTYNAME;
@@ -159,7 +133,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$response->setHeader('Content-Location', $url . $tag->getId());
// created
- $response->setStatus(201);
+ $response->setStatus(Http::STATUS_CREATED);
return false;
}
}
@@ -220,6 +194,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
return $tag;
} catch (TagAlreadyExistsException $e) {
throw new Conflict('Tag already exists', 0, $e);
+ } catch (TagCreationForbiddenException $e) {
+ throw new Forbidden('You don’t have permissions to create tags', 0, $e);
}
}
@@ -234,14 +210,14 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleGetProperties(
PropFind $propFind,
- \Sabre\DAV\INode $node
+ \Sabre\DAV\INode $node,
) {
if ($node instanceof Node) {
$this->propfindForFile($propFind, $node);
return;
}
- if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) {
+ if (!$node instanceof SystemTagNode && !$node instanceof SystemTagMappingNode && !$node instanceof SystemTagObjectType) {
return;
}
@@ -250,6 +226,10 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
}
+ $propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string {
+ return '"' . ($node->getSystemTag()->getETag() ?? '') . '"';
+ });
+
$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
return $node->getSystemTag()->getId();
});
@@ -272,6 +252,10 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
});
+ $propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) {
+ return $node->getSystemTag()->getColor() ?? '';
+ });
+
$propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
// property only available for admins
@@ -290,9 +274,25 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
return $node->getNumberOfFiles();
});
- $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int {
+ $propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int {
return $node->getReferenceFileId();
});
+
+ $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
+ $objectTypes = $this->tagMapper->getAvailableObjectTypes();
+ $objects = [];
+ foreach ($objectTypes as $type) {
+ $systemTagObjectType = new SystemTagObjectType($node->getSystemTag(), $type, $this->tagManager, $this->tagMapper);
+ $objects = array_merge($objects, array_fill_keys($systemTagObjectType->getObjectsIds(), $type));
+ }
+ return new SystemTagsObjectList($objects);
+ });
+ }
+
+ if ($node instanceof SystemTagObjectType) {
+ $propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
+ return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName()));
+ });
}
}
@@ -323,9 +323,6 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) {
$user = $this->userSession->getUser();
- if ($user === null) {
- return;
- }
$tags = $this->getTagsForFile($node->getId(), $user);
usort($tags, function (ISystemTag $tagA, ISystemTag $tagB): int {
@@ -339,8 +336,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
* @param int $fileId
* @return ISystemTag[]
*/
- private function getTagsForFile(int $fileId, IUser $user): array {
-
+ private function getTagsForFile(int $fileId, ?IUser $user): array {
if (isset($this->cachedTagMappings[$fileId])) {
$tagIds = $this->cachedTagMappings[$fileId];
} else {
@@ -384,22 +380,86 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->server->tree->getNodeForPath($path);
- if (!($node instanceof SystemTagNode)) {
+ if (!$node instanceof SystemTagNode && !$node instanceof SystemTagObjectType) {
return;
}
+ $propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) {
+ if (!$node instanceof SystemTagObjectType) {
+ return false;
+ }
+
+ if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
+ $user = $this->userSession->getUser();
+ if (!$user) {
+ throw new Forbidden('You don’t have permissions to update tags');
+ }
+
+ $propValue = $props[self::OBJECTIDS_PROPERTYNAME];
+ if (!$propValue instanceof SystemTagsObjectList || count($propValue->getObjects()) === 0) {
+ throw new BadRequest('Invalid object-ids property');
+ }
+
+ $objects = $propValue->getObjects();
+ $objectTypes = array_unique(array_values($objects));
+
+ if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) {
+ throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName());
+ }
+
+ // Only files are supported at the moment
+ // Also see SystemTagsRelationsCollection file
+ if ($objectTypes[0] !== 'files') {
+ throw new BadRequest('Invalid object-ids property type. Only files are supported');
+ }
+
+ // Get all current tagged objects
+ $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
+ $toAddObjects = array_map(fn ($value) => (string)$value, array_keys($objects));
+
+ // Compute the tags to add and remove
+ $addedObjects = array_values(array_diff($toAddObjects, $taggedObjects));
+ $removedObjects = array_values(array_diff($taggedObjects, $toAddObjects));
+
+ // Check permissions for each object to be freshly tagged or untagged
+ if (!$this->canUpdateTagForFileIds(array_merge($addedObjects, $removedObjects))) {
+ throw new Forbidden('You don’t have permissions to update tags');
+ }
+
+ $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), array_keys($objects));
+ }
+
+ if ($props[self::OBJECTIDS_PROPERTYNAME] === null) {
+ // Check the user have permissions to remove the tag from all currently tagged objects
+ $taggedObjects = $this->tagMapper->getObjectIdsForTags([$node->getSystemTag()->getId()], 'files');
+ if (!$this->canUpdateTagForFileIds($taggedObjects)) {
+ throw new Forbidden('You don’t have permissions to update tags');
+ }
+
+ $this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), []);
+ }
+
+ return true;
+ });
+
$propPatch->handle([
self::DISPLAYNAME_PROPERTYNAME,
self::USERVISIBLE_PROPERTYNAME,
self::USERASSIGNABLE_PROPERTYNAME,
self::GROUPS_PROPERTYNAME,
self::NUM_FILES_PROPERTYNAME,
- self::FILEID_PROPERTYNAME,
+ self::REFERENCE_FILEID_PROPERTYNAME,
+ self::COLOR_PROPERTYNAME,
], function ($props) use ($node) {
+ if (!$node instanceof SystemTagNode) {
+ return false;
+ }
+
$tag = $node->getSystemTag();
$name = $tag->getName();
$userVisible = $tag->isUserVisible();
$userAssignable = $tag->isUserAssignable();
+ $color = $tag->getColor();
$updateTag = false;
@@ -420,6 +480,15 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$updateTag = true;
}
+ if (isset($props[self::COLOR_PROPERTYNAME])) {
+ $propValue = $props[self::COLOR_PROPERTYNAME];
+ if ($propValue === '' || $propValue === 'null') {
+ $propValue = null;
+ }
+ $color = $propValue;
+ $updateTag = true;
+ }
+
if (isset($props[self::GROUPS_PROPERTYNAME])) {
if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
// property only available for admins
@@ -431,16 +500,40 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$this->tagManager->setTagGroups($tag, $groupIds);
}
- if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) {
+ if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::REFERENCE_FILEID_PROPERTYNAME])) {
// read-only properties
throw new Forbidden();
}
if ($updateTag) {
- $node->update($name, $userVisible, $userAssignable);
+ try {
+ $node->update($name, $userVisible, $userAssignable, $color);
+ } catch (TagUpdateForbiddenException $e) {
+ throw new Forbidden('You don’t have permissions to update tags', 0, $e);
+ }
}
return true;
});
}
+
+ /**
+ * Check if the user can update the tag for the given file ids
+ *
+ * @param list<string> $fileIds
+ * @return bool
+ */
+ private function canUpdateTagForFileIds(array $fileIds): bool {
+ $user = $this->userSession->getUser();
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ foreach ($fileIds as $fileId) {
+ $nodes = $userFolder->getById((int)$fileId);
+ foreach ($nodes as $node) {
+ if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php
index 86ccadf5f56..b854db7b94d 100644
--- a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
@@ -28,6 +11,7 @@ use OCP\IGroupManager;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
@@ -37,21 +21,6 @@ use Sabre\DAV\ICollection;
class SystemTagsByIdCollection implements ICollection {
/**
- * @var ISystemTagManager
- */
- private $tagManager;
-
- /**
- * @var IGroupManager
- */
- private $groupManager;
-
- /**
- * @var IUserSession
- */
- private $userSession;
-
- /**
* SystemTagsByIdCollection constructor.
*
* @param ISystemTagManager $tagManager
@@ -59,13 +28,11 @@ class SystemTagsByIdCollection implements ICollection {
* @param IGroupManager $groupManager
*/
public function __construct(
- ISystemTagManager $tagManager,
- IUserSession $userSession,
- IGroupManager $groupManager
+ private ISystemTagManager $tagManager,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ protected ISystemTagObjectMapper $tagMapper,
) {
- $this->tagManager = $tagManager;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
}
/**
@@ -197,6 +164,6 @@ class SystemTagsByIdCollection implements ICollection {
* @return SystemTagNode
*/
private function makeNode(ISystemTag $tag) {
- return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager);
+ return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager, $this->tagMapper);
}
}
diff --git a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
index 4ace9bde412..f11482b04ee 100644
--- a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\SystemTag;
@@ -33,29 +16,23 @@ use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\SimpleCollection;
class SystemTagsInUseCollection extends SimpleCollection {
- protected IUserSession $userSession;
- protected IRootFolder $rootFolder;
- protected string $mediaType;
- protected ISystemTagManager $systemTagManager;
protected SystemTagsInFilesDetector $systemTagsInFilesDetector;
/** @noinspection PhpMissingParentConstructorInspection */
public function __construct(
- IUserSession $userSession,
- IRootFolder $rootFolder,
- ISystemTagManager $systemTagManager,
+ protected IUserSession $userSession,
+ protected IRootFolder $rootFolder,
+ protected ISystemTagManager $systemTagManager,
+ protected ISystemTagObjectMapper $tagMapper,
SystemTagsInFilesDetector $systemTagsInFilesDetector,
- string $mediaType = ''
+ protected string $mediaType = '',
) {
- $this->userSession = $userSession;
- $this->rootFolder = $rootFolder;
- $this->systemTagManager = $systemTagManager;
- $this->mediaType = $mediaType;
$this->systemTagsInFilesDetector = $systemTagsInFilesDetector;
$this->name = 'systemtags-assigned';
if ($this->mediaType != '') {
@@ -71,7 +48,7 @@ class SystemTagsInUseCollection extends SimpleCollection {
if ($this->mediaType !== '') {
throw new NotFound('Invalid media type');
}
- return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->systemTagsInFilesDetector, $name);
+ return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->tagMapper, $this->systemTagsInFilesDetector, $name);
}
/**
@@ -96,11 +73,11 @@ class SystemTagsInUseCollection extends SimpleCollection {
$result = $this->systemTagsInFilesDetector->detectAssignedSystemTagsIn($userFolder, $this->mediaType);
$children = [];
foreach ($result as $tagData) {
- $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']);
+ $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable'], $tagData['etag'], $tagData['color']);
// read only, so we can submit the isAdmin parameter as false generally
- $node = new SystemTagNode($tag, $user, false, $this->systemTagManager);
- $node->setNumberOfFiles((int) $tagData['number_files']);
- $node->setReferenceFileId((int) $tagData['ref_file_id']);
+ $node = new SystemTagNode($tag, $user, false, $this->systemTagManager, $this->tagMapper);
+ $node->setNumberOfFiles((int)$tagData['number_files']);
+ $node->setReferenceFileId((int)$tagData['ref_file_id']);
$children[] = $node;
}
return $children;
diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectList.php b/apps/dav/lib/SystemTag/SystemTagsObjectList.php
new file mode 100644
index 00000000000..64e8b1bbebb
--- /dev/null
+++ b/apps/dav/lib/SystemTag/SystemTagsObjectList.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\SystemTag;
+
+use Sabre\Xml\Reader;
+use Sabre\Xml\Writer;
+use Sabre\Xml\XmlDeserializable;
+use Sabre\Xml\XmlSerializable;
+
+/**
+ * This property contains multiple "object-id" elements.
+ */
+class SystemTagsObjectList implements XmlSerializable, XmlDeserializable {
+
+ public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
+ public const OBJECTID_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}object-id';
+ public const OBJECTID_PROPERTYNAME = '{http://nextcloud.org/ns}id';
+ public const OBJECTTYPE_PROPERTYNAME = '{http://nextcloud.org/ns}type';
+
+ /**
+ * @param array<string, string> $objects An array of object ids and their types
+ */
+ public function __construct(
+ private array $objects,
+ ) {
+ }
+
+ /**
+ * Get the object ids and their types.
+ *
+ * @return array<string, string>
+ */
+ public function getObjects(): array {
+ return $this->objects;
+ }
+
+ public static function xmlDeserialize(Reader $reader) {
+ $tree = $reader->parseInnerTree();
+ if ($tree === null) {
+ return null;
+ }
+
+ $objects = [];
+ foreach ($tree as $elem) {
+ if ($elem['name'] === self::OBJECTID_ROOT_PROPERTYNAME) {
+ $value = $elem['value'];
+ $id = '';
+ $type = '';
+ foreach ($value as $subElem) {
+ if ($subElem['name'] === self::OBJECTID_PROPERTYNAME) {
+ $id = $subElem['value'];
+ } elseif ($subElem['name'] === self::OBJECTTYPE_PROPERTYNAME) {
+ $type = $subElem['value'];
+ }
+ }
+ if ($id !== '' && $type !== '') {
+ $objects[(string)$id] = (string)$type;
+ }
+ }
+ }
+
+ return new self($objects);
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * @param Writer $writer
+ * @return void
+ */
+ public function xmlSerialize(Writer $writer) {
+ foreach ($this->objects as $objectsId => $type) {
+ $writer->startElement(SystemTagPlugin::OBJECTIDS_PROPERTYNAME);
+ $writer->writeElement(self::OBJECTID_PROPERTYNAME, $objectsId);
+ $writer->writeElement(self::OBJECTTYPE_PROPERTYNAME, $type);
+ $writer->endElement();
+ }
+ }
+}
diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php
index b45ef6c3f71..da58f9bf308 100644
--- a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php
index f1d5fc06c99..9bd66ca0d61 100644
--- a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
diff --git a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
index 989c4640a61..0839a5bc995 100644
--- a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;
@@ -47,6 +28,8 @@ class SystemTagsRelationsCollection extends SimpleCollection {
IRootFolder $rootFolder,
) {
$children = [
+ // Only files are supported at the moment
+ // Also see SystemTagPlugin::OBJECTIDS_PROPERTYNAME supported types
new SystemTagsObjectTypeCollection(
'files',
$tagManager,
diff --git a/apps/dav/lib/Traits/PrincipalProxyTrait.php b/apps/dav/lib/Traits/PrincipalProxyTrait.php
index 6e764cac8c0..feec485fe5c 100644
--- a/apps/dav/lib/Traits/PrincipalProxyTrait.php
+++ b/apps/dav/lib/Traits/PrincipalProxyTrait.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Traits;
diff --git a/apps/dav/lib/Upload/AssemblyStream.php b/apps/dav/lib/Upload/AssemblyStream.php
index ef6d39974c0..642a8604b17 100644
--- a/apps/dav/lib/Upload/AssemblyStream.php
+++ b/apps/dav/lib/Upload/AssemblyStream.php
@@ -1,30 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Markus Goetz <markus@woboq.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
@@ -96,6 +75,10 @@ class AssemblyStream implements \Icewind\Streams\File {
$offset = $this->size + $offset;
}
+ if ($offset === $this->pos) {
+ return true;
+ }
+
if ($offset > $this->size) {
return false;
}
@@ -116,7 +99,7 @@ class AssemblyStream implements \Icewind\Streams\File {
$stream = $this->getStream($this->nodes[$nodeIndex]);
$nodeOffset = $offset - $nodeStart;
- if (fseek($stream, $nodeOffset) === -1) {
+ if ($nodeOffset > 0 && fseek($stream, $nodeOffset) === -1) {
return false;
}
$this->currentNode = $nodeIndex;
@@ -147,9 +130,14 @@ class AssemblyStream implements \Icewind\Streams\File {
}
}
- do {
+ $collectedData = '';
+ // read data until we either got all the data requested or there is no more stream left
+ while ($count > 0 && !is_null($this->currentStream)) {
$data = fread($this->currentStream, $count);
$read = strlen($data);
+
+ $count -= $read;
+ $collectedData .= $data;
$this->currentNodeRead += $read;
if (feof($this->currentStream)) {
@@ -166,14 +154,11 @@ class AssemblyStream implements \Icewind\Streams\File {
$this->currentStream = null;
}
}
- // if no data read, try again with the next node because
- // returning empty data can make the caller think there is no more
- // data left to read
- } while ($read === 0 && !is_null($this->currentStream));
+ }
// update position
- $this->pos += $read;
- return $data;
+ $this->pos += strlen($collectedData);
+ return $collectedData;
}
/**
diff --git a/apps/dav/lib/Upload/ChunkingPlugin.php b/apps/dav/lib/Upload/ChunkingPlugin.php
index 5a443ee7712..8cc8f7d6c61 100644
--- a/apps/dav/lib/Upload/ChunkingPlugin.php
+++ b/apps/dav/lib/Upload/ChunkingPlugin.php
@@ -1,32 +1,15 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, ownCloud GmbH
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2017 ownCloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCP\AppFramework\Http;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
@@ -107,7 +90,7 @@ class ChunkingPlugin extends ServerPlugin {
$response = $this->server->httpResponse;
$response->setHeader('Content-Length', '0');
- $response->setStatus($fileExists ? 204 : 201);
+ $response->setStatus($fileExists ? Http::STATUS_NO_CONTENT : Http::STATUS_CREATED);
return false;
}
diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php
index c66ffaa3f7d..07452dc0593 100644
--- a/apps/dav/lib/Upload/ChunkingV2Plugin.php
+++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php
@@ -1,26 +1,9 @@
<?php
declare(strict_types=1);
-/*
- * @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Upload;
@@ -35,6 +18,7 @@ use OC\Memcache\Redis;
use OC_Hook;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\File;
+use OCP\AppFramework\Http;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
@@ -141,7 +125,7 @@ class ChunkingV2Plugin extends ServerPlugin {
self::UPLOAD_TARGET_ID => $targetFile->getId(),
], 86400);
- $response->setStatus(201);
+ $response->setStatus(Http::STATUS_CREATED);
return true;
}
@@ -252,7 +236,7 @@ class ChunkingV2Plugin extends ServerPlugin {
$response = $this->server->httpResponse;
$response->setHeader('Content-Type', 'application/xml; charset=utf-8');
$response->setHeader('Content-Length', '0');
- $response->setStatus($destinationExists ? 204 : 201);
+ $response->setStatus($destinationExists ? Http::STATUS_NO_CONTENT : Http::STATUS_CREATED);
return false;
}
diff --git a/apps/dav/lib/Upload/CleanupService.php b/apps/dav/lib/Upload/CleanupService.php
index 2b6fc965c01..ffa6bad533c 100644
--- a/apps/dav/lib/Upload/CleanupService.php
+++ b/apps/dav/lib/Upload/CleanupService.php
@@ -3,48 +3,25 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Upload;
use OCA\DAV\BackgroundJob\UploadCleanup;
use OCP\BackgroundJob\IJobList;
-use OCP\IUserSession;
class CleanupService {
- /** @var IUserSession */
- private $userSession;
- /** @var IJobList */
- private $jobList;
-
- public function __construct(IUserSession $userSession, IJobList $jobList) {
- $this->userSession = $userSession;
- $this->jobList = $jobList;
+ public function __construct(
+ private IJobList $jobList,
+ ) {
}
- public function addJob(string $folder) {
- $this->jobList->add(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]);
+ public function addJob(string $uid, string $folder) {
+ $this->jobList->add(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
}
- public function removeJob(string $folder) {
- $this->jobList->remove(UploadCleanup::class, ['uid' => $this->userSession->getUser()->getUID(), 'folder' => $folder]);
+ public function removeJob(string $uid, string $folder) {
+ $this->jobList->remove(UploadCleanup::class, ['uid' => $uid, 'folder' => $folder]);
}
}
diff --git a/apps/dav/lib/Upload/FutureFile.php b/apps/dav/lib/Upload/FutureFile.php
index 0b158e364cf..ba37c56978d 100644
--- a/apps/dav/lib/Upload/FutureFile.php
+++ b/apps/dav/lib/Upload/FutureFile.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
@@ -36,18 +20,14 @@ use Sabre\DAV\IFile;
* @package OCA\DAV\Upload
*/
class FutureFile implements \Sabre\DAV\IFile {
- /** @var Directory */
- private $root;
- /** @var string */
- private $name;
-
/**
* @param Directory $root
* @param string $name
*/
- public function __construct(Directory $root, $name) {
- $this->root = $root;
- $this->name = $name;
+ public function __construct(
+ private Directory $root,
+ private $name,
+ ) {
}
/**
diff --git a/apps/dav/lib/Upload/PartFile.php b/apps/dav/lib/Upload/PartFile.php
index 8bfe992a987..11900997a90 100644
--- a/apps/dav/lib/Upload/PartFile.php
+++ b/apps/dav/lib/Upload/PartFile.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
@@ -32,14 +16,10 @@ use Sabre\DAV\IFile;
* but handled directly by external storage services like S3 with Multipart Upload
*/
class PartFile implements IFile {
- /** @var Directory */
- private $root;
- /** @var array */
- private $partInfo;
-
- public function __construct(Directory $root, array $partInfo) {
- $this->root = $root;
- $this->partInfo = $partInfo;
+ public function __construct(
+ private Directory $root,
+ private array $partInfo,
+ ) {
}
/**
diff --git a/apps/dav/lib/Upload/RootCollection.php b/apps/dav/lib/Upload/RootCollection.php
index e05c154c8ea..cd7ab7f5e0a 100644
--- a/apps/dav/lib/Upload/RootCollection.php
+++ b/apps/dav/lib/Upload/RootCollection.php
@@ -3,49 +3,42 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
+use OCP\Files\IRootFolder;
+use OCP\IUserSession;
+use OCP\Share\IManager;
use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalBackend;
class RootCollection extends AbstractPrincipalCollection {
- /** @var CleanupService */
- private $cleanupService;
-
- public function __construct(PrincipalBackend\BackendInterface $principalBackend,
+ public function __construct(
+ PrincipalBackend\BackendInterface $principalBackend,
string $principalPrefix,
- CleanupService $cleanupService) {
+ private CleanupService $cleanupService,
+ private IRootFolder $rootFolder,
+ private IUserSession $userSession,
+ private IManager $shareManager,
+ ) {
parent::__construct($principalBackend, $principalPrefix);
- $this->cleanupService = $cleanupService;
}
/**
* @inheritdoc
*/
public function getChildForPrincipal(array $principalInfo): UploadHome {
- return new UploadHome($principalInfo, $this->cleanupService);
+ return new UploadHome(
+ $principalInfo,
+ $this->cleanupService,
+ $this->rootFolder,
+ $this->userSession,
+ $this->shareManager,
+ );
}
/**
diff --git a/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php
new file mode 100644
index 00000000000..a7030ba1133
--- /dev/null
+++ b/apps/dav/lib/Upload/UploadAutoMkcolPlugin.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Upload;
+
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use function Sabre\Uri\split as uriSplit;
+
+/**
+ * Class that allows automatically creating non-existing collections on file
+ * upload.
+ *
+ * Since this functionality is not WebDAV compliant, it needs a special
+ * header to be activated.
+ */
+class UploadAutoMkcolPlugin extends ServerPlugin {
+
+ private Server $server;
+
+ public function initialize(Server $server): void {
+ $server->on('beforeMethod:PUT', [$this, 'beforeMethod']);
+ $this->server = $server;
+ }
+
+ /**
+ * @throws NotFound a node expected to exist cannot be found
+ */
+ public function beforeMethod(RequestInterface $request, ResponseInterface $response): bool {
+ if ($request->getHeader('X-NC-WebDAV-Auto-Mkcol') !== '1') {
+ return true;
+ }
+
+ [$path,] = uriSplit($request->getPath());
+
+ if ($this->server->tree->nodeExists($path)) {
+ return true;
+ }
+
+ $parts = explode('/', trim($path, '/'));
+ $rootPath = array_shift($parts);
+ $node = $this->server->tree->getNodeForPath('/' . $rootPath);
+
+ if (!($node instanceof ICollection)) {
+ // the root node is not a collection, let SabreDAV handle it
+ return true;
+ }
+
+ foreach ($parts as $part) {
+ if (!$node->childExists($part)) {
+ $node->createDirectory($part);
+ }
+
+ $node = $node->getChild($part);
+ }
+
+ return true;
+ }
+}
diff --git a/apps/dav/lib/Upload/UploadFile.php b/apps/dav/lib/Upload/UploadFile.php
index efe1385c8ce..7301e855cfe 100644
--- a/apps/dav/lib/Upload/UploadFile.php
+++ b/apps/dav/lib/Upload/UploadFile.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Upload;
@@ -29,11 +12,9 @@ use OCA\DAV\Connector\Sabre\File;
use Sabre\DAV\IFile;
class UploadFile implements IFile {
- /** @var File */
- private $file;
-
- public function __construct(File $file) {
- $this->file = $file;
+ public function __construct(
+ private File $file,
+ ) {
}
public function put($data) {
diff --git a/apps/dav/lib/Upload/UploadFolder.php b/apps/dav/lib/Upload/UploadFolder.php
index a1dade0e865..8890d472f87 100644
--- a/apps/dav/lib/Upload/UploadFolder.php
+++ b/apps/dav/lib/Upload/UploadFolder.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
@@ -28,21 +11,18 @@ use OC\Files\ObjectStore\ObjectStoreStorage;
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
use OCP\Files\Storage\IStorage;
+use OCP\ICacheFactory;
+use OCP\Server;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;
class UploadFolder implements ICollection {
- /** @var Directory */
- private $node;
- /** @var CleanupService */
- private $cleanupService;
- /** @var IStorage */
- private $storage;
-
- public function __construct(Directory $node, CleanupService $cleanupService, IStorage $storage) {
- $this->node = $node;
- $this->cleanupService = $cleanupService;
- $this->storage = $storage;
+ public function __construct(
+ private Directory $node,
+ private CleanupService $cleanupService,
+ private IStorage $storage,
+ private string $uid,
+ ) {
}
public function createFile($name, $data = null) {
@@ -83,7 +63,7 @@ class UploadFolder implements ICollection {
/** @var ObjectStoreStorage $storage */
$objectStore = $this->storage->getObjectStore();
if ($objectStore instanceof IObjectStoreMultiPartUpload) {
- $cache = \OC::$server->getMemCacheFactory()->createDistributed(ChunkingV2Plugin::CACHE_KEY);
+ $cache = Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
$uploadSession = $cache->get($this->getName());
if ($uploadSession) {
$uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
@@ -110,7 +90,7 @@ class UploadFolder implements ICollection {
$this->node->delete();
// Background cleanup job is not needed anymore
- $this->cleanupService->removeJob($this->getName());
+ $this->cleanupService->removeJob($this->uid, $this->getName());
}
public function getName() {
diff --git a/apps/dav/lib/Upload/UploadHome.php b/apps/dav/lib/Upload/UploadHome.php
index 6664d8c85b6..4042f1c4101 100644
--- a/apps/dav/lib/Upload/UploadHome.php
+++ b/apps/dav/lib/Upload/UploadHome.php
@@ -1,45 +1,43 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Upload;
-use OC\Files\Filesystem;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\IUserSession;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;
class UploadHome implements ICollection {
- /** @var array */
- private $principalInfo;
- /** @var CleanupService */
- private $cleanupService;
-
- public function __construct(array $principalInfo, CleanupService $cleanupService) {
- $this->principalInfo = $principalInfo;
- $this->cleanupService = $cleanupService;
+ private string $uid;
+ private ?Folder $uploadFolder = null;
+
+ public function __construct(
+ private readonly array $principalInfo,
+ private readonly CleanupService $cleanupService,
+ private readonly IRootFolder $rootFolder,
+ private readonly IUserSession $userSession,
+ private readonly \OCP\Share\IManager $shareManager,
+ ) {
+ [$prefix, $name] = \Sabre\Uri\split($principalInfo['uri']);
+ if ($prefix === 'principals/shares') {
+ $this->uid = $this->shareManager->getShareByToken($name)->getShareOwner();
+ } else {
+ $user = $this->userSession->getUser();
+ if (!$user) {
+ throw new Forbidden('Not logged in');
+ }
+
+ $this->uid = $user->getUID();
+ }
}
public function createFile($name, $data = null) {
@@ -50,16 +48,26 @@ class UploadHome implements ICollection {
$this->impl()->createDirectory($name);
// Add a cleanup job
- $this->cleanupService->addJob($name);
+ $this->cleanupService->addJob($this->uid, $name);
}
public function getChild($name): UploadFolder {
- return new UploadFolder($this->impl()->getChild($name), $this->cleanupService, $this->getStorage());
+ return new UploadFolder(
+ $this->impl()->getChild($name),
+ $this->cleanupService,
+ $this->getStorage(),
+ $this->uid,
+ );
}
public function getChildren(): array {
return array_map(function ($node) {
- return new UploadFolder($node, $this->cleanupService, $this->getStorage());
+ return new UploadFolder(
+ $node,
+ $this->cleanupService,
+ $this->getStorage(),
+ $this->uid,
+ );
}, $this->impl()->getChildren());
}
@@ -84,28 +92,29 @@ class UploadHome implements ICollection {
return $this->impl()->getLastModified();
}
- /**
- * @return Directory
- */
- private function impl() {
- $view = $this->getView();
- $rootInfo = $view->getFileInfo('');
- return new Directory($view, $rootInfo);
+ private function getUploadFolder(): Folder {
+ if ($this->uploadFolder === null) {
+ $path = '/' . $this->uid . '/uploads';
+ try {
+ $folder = $this->rootFolder->get($path);
+ if (!$folder instanceof Folder) {
+ throw new \Exception('Upload folder is a file');
+ }
+ $this->uploadFolder = $folder;
+ } catch (NotFoundException $e) {
+ $this->uploadFolder = $this->rootFolder->newFolder($path);
+ }
+ }
+ return $this->uploadFolder;
}
- private function getView() {
- $rootView = new View();
- $user = \OC::$server->getUserSession()->getUser();
- Filesystem::initMountPoints($user->getUID());
- if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
- $rootView->mkdir('/' . $user->getUID() . '/uploads');
- }
- return new View('/' . $user->getUID() . '/uploads');
+ private function impl(): Directory {
+ $folder = $this->getUploadFolder();
+ $view = new View($folder->getPath());
+ return new Directory($view, $folder);
}
private function getStorage() {
- $view = $this->getView();
- $storage = $view->getFileInfo('')->getStorage();
- return $storage;
+ return $this->getUploadFolder()->getStorage();
}
}
diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php
index 22a20703024..73e9c375490 100644
--- a/apps/dav/lib/UserMigration/CalendarMigrator.php
+++ b/apps/dav/lib/UserMigration/CalendarMigrator.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;
@@ -58,17 +41,6 @@ class CalendarMigrator implements IMigrator, ISizeEstimationMigrator {
use TMigratorBasicVersionHandling;
- private CalDavBackend $calDavBackend;
-
- private ICalendarManager $calendarManager;
-
- // ICSExportPlugin is injected as the mergeObjects() method is required and is not to be used as a SabreDAV server plugin
- private ICSExportPlugin $icsExportPlugin;
-
- private Defaults $defaults;
-
- private IL10N $l10n;
-
private SabreDavServer $sabreDavServer;
private const USERS_URI_ROOT = 'principals/users/';
@@ -80,18 +52,12 @@ class CalendarMigrator implements IMigrator, ISizeEstimationMigrator {
private const EXPORT_ROOT = Application::APP_ID . '/calendars/';
public function __construct(
- CalDavBackend $calDavBackend,
- ICalendarManager $calendarManager,
- ICSExportPlugin $icsExportPlugin,
- Defaults $defaults,
- IL10N $l10n
+ private CalDavBackend $calDavBackend,
+ private ICalendarManager $calendarManager,
+ private ICSExportPlugin $icsExportPlugin,
+ private Defaults $defaults,
+ private IL10N $l10n,
) {
- $this->calDavBackend = $calDavBackend;
- $this->calendarManager = $calendarManager;
- $this->icsExportPlugin = $icsExportPlugin;
- $this->defaults = $defaults;
- $this->l10n = $l10n;
-
$root = new RootCollection();
$this->sabreDavServer = new SabreDavServer(new CachingTree($root));
$this->sabreDavServer->addPlugin(new CalDAVPlugin());
diff --git a/apps/dav/lib/UserMigration/CalendarMigratorException.php b/apps/dav/lib/UserMigration/CalendarMigratorException.php
index 3b4f8f89232..f3754809b44 100644
--- a/apps/dav/lib/UserMigration/CalendarMigratorException.php
+++ b/apps/dav/lib/UserMigration/CalendarMigratorException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;
diff --git a/apps/dav/lib/UserMigration/ContactsMigrator.php b/apps/dav/lib/UserMigration/ContactsMigrator.php
index 38c53a1f76e..96d623938a3 100644
--- a/apps/dav/lib/UserMigration/ContactsMigrator.php
+++ b/apps/dav/lib/UserMigration/ContactsMigrator.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;
@@ -54,10 +37,6 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator {
use TMigratorBasicVersionHandling;
- private CardDavBackend $cardDavBackend;
-
- private IL10N $l10n;
-
private SabreDavServer $sabreDavServer;
private const USERS_URI_ROOT = 'principals/users/';
@@ -71,12 +50,9 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator {
private const PATH_ROOT = Application::APP_ID . '/address_books/';
public function __construct(
- CardDavBackend $cardDavBackend,
- IL10N $l10n
+ private CardDavBackend $cardDavBackend,
+ private IL10N $l10n,
) {
- $this->cardDavBackend = $cardDavBackend;
- $this->l10n = $l10n;
-
$root = new RootCollection();
$this->sabreDavServer = new SabreDavServer(new CachingTree($root));
$this->sabreDavServer->addPlugin(new CardDAVPlugin());
@@ -268,7 +244,7 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator {
$vCard->serialize(),
);
} catch (Throwable $e) {
- $output->writeln("Error creating contact \"" . ($vCard->FN ?? 'null') . "\" from \"$filename\", skipping…");
+ $output->writeln('Error creating contact "' . ($vCard->FN ?? 'null') . "\" from \"$filename\", skipping…");
}
}
diff --git a/apps/dav/lib/UserMigration/ContactsMigratorException.php b/apps/dav/lib/UserMigration/ContactsMigratorException.php
index 63dedebf73d..8d64d3d4428 100644
--- a/apps/dav/lib/UserMigration/ContactsMigratorException.php
+++ b/apps/dav/lib/UserMigration/ContactsMigratorException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;
diff --git a/apps/dav/lib/UserMigration/InvalidAddressBookException.php b/apps/dav/lib/UserMigration/InvalidAddressBookException.php
index fd99eac1a73..d904dd9d4dd 100644
--- a/apps/dav/lib/UserMigration/InvalidAddressBookException.php
+++ b/apps/dav/lib/UserMigration/InvalidAddressBookException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;
diff --git a/apps/dav/lib/UserMigration/InvalidCalendarException.php b/apps/dav/lib/UserMigration/InvalidCalendarException.php
index 0e42ef1bc20..664989de8b1 100644
--- a/apps/dav/lib/UserMigration/InvalidCalendarException.php
+++ b/apps/dav/lib/UserMigration/InvalidCalendarException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\UserMigration;